pax_global_header00006660000000000000000000000064151706676040014526gustar00rootroot0000000000000052 comment=df7695b297c4798b7f904c4ab4cf4bf386d0c72b go-openapi-jsonpointer-d29978d/000077500000000000000000000000001517066760400165105ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.claude/000077500000000000000000000000001517066760400200235ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.claude/.gitignore000066400000000000000000000000501517066760400220060ustar00rootroot00000000000000plans/ skills/ commands/ agents/ hooks/ go-openapi-jsonpointer-d29978d/.claude/CLAUDE.md000066400000000000000000000041421517066760400213030ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Go implementation of [JSON Pointer (RFC 6901)](https://datatracker.ietf.org/doc/html/rfc6901) for navigating and mutating JSON documents represented as Go values. Unlike most implementations, it works not only with `map[string]any` and slices, but also with Go structs (resolved via `json` struct tags and reflection). See [docs/MAINTAINERS.md](../docs/MAINTAINERS.md) for CI/CD, release process, and repo structure details. ### Package layout (single package) | File | Contents | |------|----------| | `pointer.go` | Core types (`Pointer`, `JSONPointable`, `JSONSetable`), `New`, `Get`, `Set`, `Offset`, `Escape`/`Unescape` | | `errors.go` | Sentinel errors: `ErrPointer`, `ErrInvalidStart`, `ErrUnsupportedValueType` | ### Key API - `New(string) (Pointer, error)` — parse a JSON pointer string (e.g. `"/foo/0/bar"`) - `Pointer.Get(document any) (any, reflect.Kind, error)` — retrieve a value - `Pointer.Set(document, value any) (any, error)` — set a value (document must be pointer/map/slice) - `Pointer.Offset(jsonString string) (int64, error)` — byte offset of token in raw JSON - `GetForToken` / `SetForToken` — single-level convenience helpers - `Escape` / `Unescape` — RFC 6901 token escaping (`~0` ↔ `~`, `~1` ↔ `/`) Custom types can implement `JSONPointable` (for Get) or `JSONSetable` (for Set) to bypass reflection. ### Dependencies - `github.com/go-openapi/swag/jsonname` — struct tag → JSON field name resolution - `github.com/go-openapi/testify/v2` — test-only assertions ### Notable historical design decisions See also .claude/plans/ROADMAP.md. - Struct fields **must** have a `json` tag to be reachable; untagged fields are ignored (differs from `encoding/json` which defaults to the Go field name). - Anonymous embedded struct fields are traversed only if tagged. - The RFC 6901 `"-"` array suffix is supported on `Pointer.Set` as an append operation (RFC 6902 convention). On `Pointer.Get` / `Pointer.Offset` it is always an error per RFC 6901 §4. go-openapi-jsonpointer-d29978d/.claude/rules/000077500000000000000000000000001517066760400211555ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.claude/rules/contributions.md000066400000000000000000000026151517066760400244050ustar00rootroot00000000000000--- paths: - "**/*" --- # Contribution rules (go-openapi) Read `.github/CONTRIBUTING.md` before opening a pull request. ## Commit hygiene - Every commit **must** be DCO signed-off (`git commit -s`) with a real email address. PGP-signed commits are appreciated but not required. - Agents may be listed as co-authors (`Co-Authored-By:`) but the commit **author must be the human sponsor**. We do not accept commits solely authored by bots or agents. - Squash commits into logical units of work before requesting review (`git rebase -i`). ## Linting Before pushing, verify your changes pass linting against the base branch: ```sh golangci-lint run --new-from-rev master ``` Install the latest version if you don't have it: ```sh go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest ``` ## Problem statement - Clearly describe the problem the PR solves, or reference an existing issue. - PR descriptions must not be vague ("fix bug", "improve code") — explain *what* was wrong and *why* the change is correct. ## Tests are mandatory - Every bug fix or feature **must** include tests that demonstrate the problem and verify the fix. - The only exceptions are documentation changes and typo fixes. - Aim for at least 80% coverage of your patch. - Run the full test suite before submitting: For mono-repos: ```sh go test work ./... ``` For single module repos: ```sh go test ./... ``` go-openapi-jsonpointer-d29978d/.claude/rules/github-workflows-conventions.md000066400000000000000000000206451517066760400273660ustar00rootroot00000000000000--- paths: - ".github/workflows/**.yml" - ".github/workflows/**.yaml" --- # GitHub Actions Workflows Formatting and Style Conventions This rule captures YAML and bash formatting rules to provide a consistent maintainer's experience across CI workflows. ## File Structure **REQUIRED**: All github action workflows are organized as a flat structure beneath `.github/workflows/`. > GitHub does not support a hierarchical organization for workflows yet. **REQUIRED**: YAML files are conventionally named `{workflow}.yml`, with the `.yml` extension. ## Code Style & Formatting ### Expression Spacing **REQUIRED**: All GitHub Actions expressions must have spaces inside the braces: ```yaml # ✅ CORRECT env: PR_URL: ${{ github.event.pull_request.html_url }} TOKEN: ${{ secrets.GITHUB_TOKEN }} # ❌ WRONG env: PR_URL: ${{github.event.pull_request.html_url}} TOKEN: ${{secrets.GITHUB_TOKEN}} ``` > Provides a consistent formatting rule. ### Conditional Syntax **REQUIRED**: Always use `${{ }}` in `if:` conditions: ```yaml # ✅ CORRECT if: ${{ inputs.enable-signing == 'true' }} if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} # ❌ WRONG (works but inconsistent) if: inputs.enable-signing == 'true' ``` > Provides a consistent formatting rule. ### GitHub Workflow Commands **REQUIRED**: Use workflow commands for status messages that should appear as annotations, with **double colon separator**: ```bash # ✅ CORRECT - Double colon (::) separator after title echo "::notice title=build::Build completed successfully" echo "::warning title=race-condition::Merge already in progress" echo "::error title=deployment::Failed to deploy" # ❌ WRONG - Single colon separator (won't render as annotation) echo "::notice title=build:Build completed" # Missing second ':' echo "::warning title=x:message" # Won't display correctly ``` **Syntax pattern:** `::LEVEL title=TITLE::MESSAGE` - `LEVEL`: notice, warning, or error - Double `::` separator is required between title and message > Wrong syntax may raise untidy warnings and produce botched output. ### YAML arrays formatting For steps, YAML arrays are formatted with the following indentation: ```yaml # ✅ CORRECT - Clear spacing between steps steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # ❌ WRONG - Dense format, more difficult to read steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # ❌ WRONG - YAML comment or blank line could be avoided steps: # - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ``` ## Security Best Practices ### Version Pinning using SHAs **REQUIRED**: Always pin action versions to commit SHAs: > Runs must be repeatable with known pinned version. Automated updates are pushed frequently (e.g. daily or weekly) > to keep pinned versions up-to-date. ```yaml # ✅ CORRECT - Pinned to commit SHA with version comment uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 # ❌ WRONG - Mutable tag reference uses: actions/checkout@v6 ``` ### Permission settings **REQUIRED**: Always set minimal permissions at the workflow level. ```yaml # ✅ CORRECT - Workflow level permissions set to minimum permissions: contents: read # ❌ WRONG - Workflow level permissions with undue privilege escalation permissions: contents: write pull-requests: write ``` **REQUIRED**: Whenever a job needs elevated privileges, always raise required permissions at the job level. ```yaml # ✅ CORRECT - Job level permissions set to the specific requirements for that job jobs: dependabot: permissions: contents: write pull-requests: write uses: ./.github/workflows/auto-merge.yml secrets: inherit # ❌ WRONG - Same permissions but set at workflow level instead of job level permissions: contents: write pull-requests: write ``` > (Security best practice detected by CodeQL analysis) ### Undue secret exposure **NEVER** use `secrets[inputs.name]` — always use explicit secret parameters. > Using keyed access to secrets forces the runner to expose ALL secrets to the job, which causes a security risk > (caught and reported by CodeQL security analysis). ```yaml # ❌ SECURITY VULNERABILITY # This exposes ALL organization and repository secrets to the runner on: workflow_call: inputs: secret-name: type: string jobs: my-job: steps: - uses: some-action@v1 with: token: ${{ secrets[inputs.secret-name] }} # ❌ DANGEROUS! ``` **SOLUTION**: Use explicit secret parameters with fallback for defaults: ```yaml # ✅ SECURE on: workflow_call: secrets: gpg-private-key: required: false jobs: my-job: steps: - uses: go-openapi/gh-actions/ci-jobs/bot-credentials@master with: # Falls back to go-openapi default if not explicitly passed gpg-private-key: ${{ secrets.gpg-private-key || secrets.CI_BOT_GPG_PRIVATE_KEY }} ``` ## Common Gotchas ### Description fields containing parsable expressions **REQUIRED**: **DO NOT** use `${{ }}` expressions in description fields: > They may be parsed by the runner, wrongly interpreted or causing failure (e.g. "not defined in this context"). ```yaml # ❌ WRONG - Can cause YAML parsing errors description: | Pass it as: gpg-private-key: ${{ secrets.MY_KEY }} # ✅ CORRECT description: | Pass it as: secrets.MY_KEY ``` ### Boolean inputs **Boolean inputs are forbidden**: NEVER use `type: boolean` for workflow inputs due to unpredictable type coercion > gh-action expressions using boolean job inputs are hard to predict and come with many quirks. ```yaml # ❌ FORBIDDEN - Boolean inputs have type coercion issues on: workflow_call: inputs: enable-feature: type: boolean # ❌ NEVER USE THIS default: true # The pattern `x == 'true' || x == true` seems safe but fails when: # - x is not a boolean: `x == true` evaluates to true if x != null # - Type coercion is unpredictable and error-prone # ✅ CORRECT - Always use string type for boolean-like inputs on: workflow_call: inputs: enable-feature: type: string # ✅ Use string instead default: 'true' # String value jobs: my-job: # Simple, reliable comparison if: ${{ inputs.enable-feature == 'true' }} # ✅ In bash, this works perfectly (inputs are always strings in bash): if [[ '${{ inputs.enable-feature }}' == 'true' ]]; then echo "Feature enabled" fi ``` **Rule**: Use `type: string` with values `'true'` or `'false'` for all boolean-like workflow inputs. **Note**: Step outputs and bash variables are always strings, so `x == 'true'` works fine for those. ### YAML fold scalars in action inputs **NEVER** use `>` or `>-` (fold scalars) for `with:` input values: > The YAML spec says fold scalars replace newlines with spaces, but the GitHub Actions runner > does not reliably honor this for action inputs. The action receives the literal multi-line string > instead of a single folded line, which breaks flag parsing. ```yaml # ❌ BROKEN - Fold scalar, args received with embedded newlines - uses: goreleaser/goreleaser-action@... with: args: >- release --clean --release-notes /tmp/notes.md # ✅ CORRECT - Single line - uses: goreleaser/goreleaser-action@... with: args: release --clean --release-notes /tmp/notes.md # ✅ CORRECT - Literal block scalar (|) is fine for run: scripts - run: | echo "line 1" echo "line 2" ``` **Rule**: Use single-line strings for `with:` inputs. Only use `|` (literal block scalar) for `run:` scripts where multi-line is intentional. go-openapi-jsonpointer-d29978d/.claude/rules/go-conventions.md000066400000000000000000000004621517066760400244510ustar00rootroot00000000000000--- paths: - "**/*.go" --- # Code conventions (go-openapi) - All files must have SPDX license headers (Apache-2.0). - Go version policy: support the 2 latest stable Go minor versions. - Commits require DCO sign-off (`git commit -s`). - use `golangci-lint fmt` to format code (not `gofmt` or `gofumpt`) go-openapi-jsonpointer-d29978d/.claude/rules/linting.md000066400000000000000000000006251517066760400231460ustar00rootroot00000000000000--- paths: - "**/*.go" --- # Linting conventions (go-openapi) ```sh golangci-lint run ``` Config: `.golangci.yml` — posture is `default: all` with explicit disables. See `docs/STYLE.md` for the rationale behind each disabled linter. Key rules: - Every `//nolint` directive **must** have an inline comment explaining why. - Prefer disabling a linter over scattering `//nolint` across the codebase. go-openapi-jsonpointer-d29978d/.claude/rules/testing.md000066400000000000000000000017121517066760400231550ustar00rootroot00000000000000--- paths: - "**/*_test.go" --- # Testing conventions (go-openapi) ## Running tests **Single module repos:** ```sh go test ./... ``` **Mono-repos (with `go.work`):** ```sh # All modules go test work ./... # Single module go test ./conv/... ``` Note: in mono-repos, plain `go test ./...` only tests the root module. The `work` pattern expands to all modules listed in `go.work`. CI runs tests on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race` via `gotestsum`. ## Fuzz tests ```sh # List all fuzz targets go test -list Fuzz ./... # Run a specific target (go test -fuzz cannot span multiple packages) go test -fuzz=Fuzz -run='FuzzTargetName$' -fuzztime=1m30s ./package ``` Fuzz corpus lives in `testdata/fuzz/` within each package. CI runs each fuzz target for 1m30s with a 5m minimize timeout. ## Test framework `github.com/go-openapi/testify/v2` — a zero-dep fork of `stretchr/testify`. Because it's a fork, `testifylint` does not work. go-openapi-jsonpointer-d29978d/.editorconfig000066400000000000000000000010331517066760400211620ustar00rootroot00000000000000# top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true # Set default charset [*.{js,py,go,scala,rb,java,html,css,less,sass,md}] charset = utf-8 # Tab indentation (no size specified) [*.go] indent_style = tab [*.md] trim_trailing_whitespace = false # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_style = space indent_size = 2 go-openapi-jsonpointer-d29978d/.github/000077500000000000000000000000001517066760400200505ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.github/copilot000077700000000000000000000000001517066760400242422../.claude/rulesustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.github/copilot-instructions.md000066400000000000000000000044011517066760400246040ustar00rootroot00000000000000# Copilot Instructions ## Project Overview Go implementation of [JSON Pointer (RFC 6901)](https://datatracker.ietf.org/doc/html/rfc6901) for navigating and mutating JSON documents represented as Go values. Works with `map[string]any`, slices, and Go structs (resolved via `json` struct tags and reflection). ## Package Layout (single package) | File | Contents | |------|----------| | `pointer.go` | Core types (`Pointer`, `JSONPointable`, `JSONSetable`), `New`, `Get`, `Set`, `Offset`, `Escape`/`Unescape` | | `errors.go` | Sentinel errors: `ErrPointer`, `ErrInvalidStart`, `ErrUnsupportedValueType` | ## Key API - `New(string) (Pointer, error)` — parse a JSON pointer string (e.g. `"/foo/0/bar"`) - `Pointer.Get(document any) (any, reflect.Kind, error)` — retrieve a value - `Pointer.Set(document, value any) (any, error)` — set a value (document must be pointer/map/slice) - `Pointer.Offset(jsonString string) (int64, error)` — byte offset of token in raw JSON - `GetForToken` / `SetForToken` — single-level convenience helpers - `Escape` / `Unescape` — RFC 6901 token escaping (`~0` ↔ `~`, `~1` ↔ `/`) Custom types can implement `JSONPointable` (for Get) or `JSONSetable` (for Set) to bypass reflection. ## Design Decisions - Struct fields **must** have a `json` tag to be reachable; untagged fields are ignored. - Anonymous embedded struct fields are traversed only if tagged. - The RFC 6901 `"-"` array suffix (append) is **not** implemented. ## Dependencies - `github.com/go-openapi/swag/jsonname` — struct tag to JSON field name resolution - `github.com/go-openapi/testify/v2` — test-only assertions (zero-dep fork of `stretchr/testify`) ## Conventions - All `.go` files must have SPDX license headers (Apache-2.0). - Commits require DCO sign-off (`git commit -s`). - Linting: `golangci-lint run` — config in `.golangci.yml` (posture: `default: all` with explicit disables). - Every `//nolint` directive **must** have an inline comment explaining why. - Tests: `go test ./...` with `-race`. CI runs on `{ubuntu, macos, windows} x {stable, oldstable}`. - Test framework: `github.com/go-openapi/testify/v2` (not `stretchr/testify`). See `.github/copilot/` (symlinked to `.claude/rules/`) for detailed rules on Go conventions, linting, testing, and contributions. go-openapi-jsonpointer-d29978d/.github/dependabot.yaml000066400000000000000000000032001517066760400230340ustar00rootroot00000000000000version: 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/*" go-openapi-jsonpointer-d29978d/.github/wordlist.txt000066400000000000000000000004531517066760400224620ustar00rootroot00000000000000CodeFactor CodeQL DCO GoDoc JSON Maintainer's PR's PRs Repo SPDX TODOs Triaging UI XYZ agentic ci codebase codecov config dependabot dev developercertificate github godoc golang golangci jsonpointer linter's linters maintainer's md metalinter monorepo openapi prepended repos semver sexualized vuln go-openapi-jsonpointer-d29978d/.github/workflows/000077500000000000000000000000001517066760400221055ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/.github/workflows/auto-merge.yml000066400000000000000000000004621517066760400246770ustar00rootroot00000000000000name: Dependabot auto-merge permissions: contents: read on: pull_request: jobs: dependabot: permissions: contents: write pull-requests: write uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/bump-release.yml000066400000000000000000000017451517066760400252200ustar00rootroot00000000000000name: Bump Release permissions: contents: read on: workflow_dispatch: inputs: bump-type: description: Type of bump (patch, minor, major) type: choice options: - patch - minor - major default: patch required: false tag-message-title: description: Tag message title to prepend to the release notes required: false type: string tag-message-body: description: | Tag message body to prepend to the release notes. (use "|" to replace end of line). required: false type: string jobs: bump-release: permissions: contents: write uses: go-openapi/ci-workflows/.github/workflows/bump-release.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 with: bump-type: ${{ inputs.bump-type }} tag-message-title: ${{ inputs.tag-message-title }} tag-message-body: ${{ inputs.tag-message-body }} secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/codeql.yml000066400000000000000000000007311517066760400241000ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "master" ] pull_request: branches: [ "master" ] paths-ignore: # remove this clause if CodeQL is a required check - '**/*.md' schedule: - cron: '39 19 * * 5' permissions: contents: read jobs: codeql: permissions: contents: read security-events: write uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/contributors.yml000066400000000000000000000005301517066760400253630ustar00rootroot00000000000000name: Contributors on: schedule: - cron: '18 4 * * 6' workflow_dispatch: permissions: contents: read jobs: contributors: permissions: pull-requests: write contents: write uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/go-test.yml000066400000000000000000000004251517066760400242130ustar00rootroot00000000000000name: go test permissions: pull-requests: read contents: read on: push: branches: - master pull_request: jobs: test: uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/scanner.yml000066400000000000000000000005751517066760400242700ustar00rootroot00000000000000name: Vulnerability scans on: branch_protection_rule: push: branches: [ "master" ] schedule: - cron: '18 4 * * 3' permissions: contents: read jobs: scanners: permissions: contents: read security-events: write uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@6ed4490472a56b1d952231565aac80f13c2d143c # V0.1.1 secrets: inherit go-openapi-jsonpointer-d29978d/.github/workflows/tag-release.yml000066400000000000000000000005451517066760400250250ustar00rootroot00000000000000name: Release on tag permissions: contents: read on: push: tags: - v[0-9]+* jobs: gh-release: name: Create release permissions: contents: write uses: go-openapi/ci-workflows/.github/workflows/release.yml@6ed4490472a56b1d952231565aac80f13c2d143c # v0.2.16 with: tag: ${{ github.ref_name }} secrets: inherit go-openapi-jsonpointer-d29978d/.gitignore000066400000000000000000000000411517066760400204730ustar00rootroot00000000000000*.out *.cov .idea .env .mcp.json go-openapi-jsonpointer-d29978d/.golangci.yml000066400000000000000000000022651517066760400211010ustar00rootroot00000000000000version: "2" linters: default: all disable: - depguard - funlen - godox - exhaustruct - nlreturn - nonamedreturns - noinlineerr - paralleltest - recvcheck - testpackage - thelper - tparallel - varnamelen - whitespace - wrapcheck - wsl - wsl_v5 settings: dupl: threshold: 200 goconst: min-len: 2 min-occurrences: 3 cyclop: max-complexity: 20 gocyclo: min-complexity: 20 exhaustive: default-signifies-exhaustive: true default-case-required: true lll: line-length: 180 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports - gofumpt exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ issues: # Maximum issues count per one linter. # Set to 0 to disable. # Default: 50 max-issues-per-linter: 0 # Maximum count of issues with the same text. # Set to 0 to disable. # Default: 3 max-same-issues: 0 go-openapi-jsonpointer-d29978d/AGENTS.md000077700000000000000000000000001517066760400261032.github/copilot-instructions.mdustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/CODE_OF_CONDUCT.md000066400000000000000000000062471517066760400213200ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ go-openapi-jsonpointer-d29978d/CONTRIBUTORS.md000066400000000000000000000027431517066760400207750ustar00rootroot00000000000000# Contributors - Repository: ['go-openapi/jsonpointer'] | Total Contributors | Total Contributions | | --- | --- | | 13 | 111 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | | @fredbi | 63 | | | @casualjim | 33 | | | @magodo | 3 | | | @youyuanwu | 3 | | | @gaiaz-iusipov | 1 | | | @gbjk | 1 | | | @gordallott | 1 | | | @ianlancetaylor | 1 | | | @mfleader | 1 | | | @Neo2308 | 1 | | | @alexandear | 1 | | | @olivierlemasle | 1 | | | @testwill | 1 | | _this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_ go-openapi-jsonpointer-d29978d/LICENSE000066400000000000000000000261351517066760400175240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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-openapi-jsonpointer-d29978d/NOTICE000066400000000000000000000030621517066760400174150ustar00rootroot00000000000000Copyright 2015-2025 go-swagger maintainers // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 This software library, github.com/go-openapi/jsonpointer, includes software developed by the go-swagger and go-openapi maintainers ("go-swagger maintainers"). Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. This software is copied from, derived from, and inspired by other original software products. It ships with copies of other software which license terms are recalled below. The original software was authored on 25-02-2013 by sigu-399 (https://github.com/sigu-399, sigu.399@gmail.com). github.com/sigu-399/jsonpointer =========================== // SPDX-FileCopyrightText: Copyright 2013 sigu-399 ( https://github.com/sigu-399 ) // SPDX-License-Identifier: Apache-2.0 Copyright 2013 sigu-399 ( https://github.com/sigu-399 ) 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-openapi-jsonpointer-d29978d/README.md000066400000000000000000000154501517066760400177740ustar00rootroot00000000000000# jsonpointer [![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url] [![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] [![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] --- An implementation of JSON Pointer for golang, which supports go `struct`. ## Announcements * **2026-04-15** : added support for trailing "-" for arrays (v0.23.0) * this brings full support of [RFC6901][RFC6901] * this is supported for types relying on the reflection-based implemented * API semantics remain essentially unaltered. Exception: `Pointer.Set(document any,value any) (document any, err error)` can only perform a best-effort to mutate the input document in place. In the case of adding elements to an array with a trailing "-", either pass a mutable array (`*[]T`) as the input document, or use the returned updated document instead. * types that implement the `JSONSetable` interface may not implement the mutation implied by the trailing "-" * **2026-04-15** : added support for optional alternate JSON name providers * for struct support the defaults might not suit all situations: there are known limitations when it comes to handle untagged fields or embedded types. * the default name provider in use is not fully aligned with go JSON stdlib * exposed an option (or global setting) to change the provider that resolves a struct into json keys * the default behavior is not altered * a new alternate name provider is added (imported from `go-openapi/swag/jsonname`), aligned with JSON stdlib behavior ## Status API is stable and feature-complete. ## Import this library in your project ```cmd go get github.com/go-openapi/jsonpointer ``` ## Basic usage See also some [examples](./examples_test.go) ### Retrieving a value ```go import ( "github.com/go-openapi/jsonpointer" ) var doc any ... pointer, err := jsonpointer.New("/foo/1") if err != nil { ... // error: e.g. invalid JSON pointer specification } value, kind, err := pointer.Get(doc) if err != nil { ... // error: e.g. key not found, index out of bounds, etc. } ... ``` ### Setting a value ```go ... var doc any ... pointer, err := jsonpointer.New("/foo/1") if err != nil { ... // error: e.g. invalid JSON pointer specification } doc, err = p.Set(doc, "value") if err != nil { ... // error: e.g. key not found, index out of bounds, etc. } ``` ## Change log See ## References also known as [RFC6901][RFC6901]. ## Licensing This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the pieces of software on top of which it has been built. ## Limitations * [RFC6901][RFC6901] is now fully supported, including trailing "-" semantics for arrays (for `Set` operations). * Default behavior: JSON name detection in go `struct`s - Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored. - anonymous fields are not traversed if untagged - the above limitations may be overcome by calling `UseGoNameProvider()` at initialization time. - alternatively, users may inject the desired custom behavior for naming fields as an option. ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) * [Contributing guidelines][contributing-doc-site] * [Maintainers documentation][maintainers-doc-site] * [Code style][style-doc-site] ## Cutting a new release Maintainers can cut a new release by either: * running [this workflow](https://github.com/go-openapi/jsonpointer/actions/workflows/bump-release.yml) * or pushing a semver tag * signed tags are preferred * The tag message is prepended to release notes [test-badge]: https://github.com/go-openapi/jsonpointer/actions/workflows/go-test.yml/badge.svg [test-url]: https://github.com/go-openapi/jsonpointer/actions/workflows/go-test.yml [cov-badge]: https://codecov.io/gh/go-openapi/jsonpointer/branch/master/graph/badge.svg [cov-url]: https://codecov.io/gh/go-openapi/jsonpointer [vuln-scan-badge]: https://github.com/go-openapi/jsonpointer/actions/workflows/scanner.yml/badge.svg [vuln-scan-url]: https://github.com/go-openapi/jsonpointer/actions/workflows/scanner.yml [codeql-badge]: https://github.com/go-openapi/jsonpointer/actions/workflows/codeql.yml/badge.svg [codeql-url]: https://github.com/go-openapi/jsonpointer/actions/workflows/codeql.yml [release-badge]: https://badge.fury.io/gh/go-openapi%2Fjsonpointer.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Fjsonpointer [gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/jsonpointer [gocard-url]: https://goreportcard.com/report/github.com/go-openapi/jsonpointer [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/jsonpointer [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/jsonpointer [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/jsonpointer [godoc-url]: http://pkg.go.dev/github.com/go-openapi/jsonpointer [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg [license-url]: https://github.com/go-openapi/jsonpointer/?tab=Apache-2.0-1-ov-file#readme [goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/jsonpointer [goversion-url]: https://github.com/go-openapi/jsonpointer/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/jsonpointer [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/jsonpointer/latest [RFC6901]: https://www.rfc-editor.org/rfc/rfc6901 [contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html [maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html [style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html go-openapi-jsonpointer-d29978d/SECURITY.md000066400000000000000000000026031517066760400203020ustar00rootroot00000000000000# Security Policy This policy outlines the commitment and practices of the go-openapi maintainers regarding security. ## Supported Versions | Version | Supported | | ------- | ------------------ | | O.x | :white_check_mark: | ## Vulnerability checks in place This repository uses automated vulnerability scans, at every merged commit and at least once a week. We use: * [`GitHub CodeQL`][codeql-url] * [`trivy`][trivy-url] * [`govulncheck`][govulncheck-url] Reports are centralized in github security reports and visible only to the maintainers. ## Reporting a vulnerability If you become aware of a security vulnerability that affects the current repository, **please report it privately to the maintainers** rather than opening a publicly visible GitHub issue. Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url]. > [!NOTE] > On Github, navigate to the project's "Security" tab then click on "Report a vulnerability". [codeql-url]: https://github.com/github/codeql [trivy-url]: https://trivy.dev/docs/latest/getting-started [govulncheck-url]: https://go.dev/blog/govulncheck [github-guidance-url]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability go-openapi-jsonpointer-d29978d/dash_token_test.go000066400000000000000000000135661517066760400222300ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "errors" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) // RFC 6901 §4: the "-" token refers to the (nonexistent) element after the // last array element. It is always an error on Get/Offset, valid only as // the terminal token of a Set against a slice (append, per RFC 6902). func TestDashToken_GetAlwaysErrors(t *testing.T) { t.Parallel() t.Run("terminal dash on slice in map", func(t *testing.T) { doc := map[string]any{"arr": []any{1, 2, 3}} p, err := New("/arr/-") require.NoError(t, err) _, _, err = p.Get(doc) require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) require.ErrorIs(t, err, ErrPointer) }) t.Run("terminal dash on top-level slice", func(t *testing.T) { doc := []int{1, 2, 3} p, err := New("/-") require.NoError(t, err) _, _, err = p.Get(doc) require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) }) t.Run("intermediate dash during get", func(t *testing.T) { doc := map[string]any{"arr": []any{map[string]any{"x": 1}}} p, err := New("/arr/-/x") require.NoError(t, err) _, _, err = p.Get(doc) require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) }) t.Run("GetForToken on slice with dash", func(t *testing.T) { _, _, err := GetForToken([]int{1, 2}, "-") require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) }) t.Run("dash on map key is a regular lookup, not an error", func(t *testing.T) { // "-" is only special for arrays. A literal "-" key in a map is fine. doc := map[string]any{"-": 42} p, err := New("/-") require.NoError(t, err) v, _, err := p.Get(doc) require.NoError(t, err) assert.Equal(t, 42, v) }) } func TestDashToken_OffsetErrors(t *testing.T) { t.Parallel() doc := `{"arr":[1,2,3]}` p, err := New("/arr/-") require.NoError(t, err) _, err = p.Offset(doc) require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) } func TestDashToken_SetAppend(t *testing.T) { t.Parallel() t.Run("append into slice nested in a map (in place)", func(t *testing.T) { doc := map[string]any{"arr": []any{1, 2}} p, err := New("/arr/-") require.NoError(t, err) out, err := p.Set(doc, 3) require.NoError(t, err) // returned doc is the same map reference assert.Equal(t, doc, out) // map's slice was rebound in place arr, ok := doc["arr"].([]any) require.True(t, ok) assert.Equal(t, []any{1, 2, 3}, arr) }) t.Run("append into top-level slice passed by value (return value is source of truth)", func(t *testing.T) { doc := []int{1, 2} p, err := New("/-") require.NoError(t, err) out, err := p.Set(doc, 3) require.NoError(t, err) // returned doc has the appended element outSlice, ok := out.([]int) require.True(t, ok) assert.Equal(t, []int{1, 2, 3}, outSlice) }) t.Run("append into top-level *[]T (in place)", func(t *testing.T) { doc := []int{1, 2} p, err := New("/-") require.NoError(t, err) _, err = p.Set(&doc, 3) require.NoError(t, err) // caller's slice variable now has the appended element assert.Equal(t, []int{1, 2, 3}, doc) }) t.Run("append into struct slice field reached via pointer (in place)", func(t *testing.T) { type holder struct { Arr []int `json:"arr"` } doc := &holder{Arr: []int{1, 2}} p, err := New("/arr/-") require.NoError(t, err) _, err = p.Set(doc, 3) require.NoError(t, err) assert.Equal(t, []int{1, 2, 3}, doc.Arr) }) t.Run("append into deeply nested slice", func(t *testing.T) { doc := map[string]any{ "outer": []any{ map[string]any{"inner": []any{"a"}}, }, } p, err := New("/outer/0/inner/-") require.NoError(t, err) _, err = p.Set(doc, "b") require.NoError(t, err) outer, ok := doc["outer"].([]any) require.True(t, ok) first, ok := outer[0].(map[string]any) require.True(t, ok) inner, ok := first["inner"].([]any) require.True(t, ok) assert.Equal(t, []any{"a", "b"}, inner) }) t.Run("SetForToken with dash appends", func(t *testing.T) { out, err := SetForToken([]int{1, 2}, "-", 3) require.NoError(t, err) outSlice, ok := out.([]int) require.True(t, ok) assert.Equal(t, []int{1, 2, 3}, outSlice) }) } func TestDashToken_SetErrors(t *testing.T) { t.Parallel() t.Run("intermediate dash is rejected", func(t *testing.T) { doc := map[string]any{"arr": []any{1, 2}} p, err := New("/arr/-/x") require.NoError(t, err) _, err = p.Set(doc, 3) require.Error(t, err) require.ErrorIs(t, err, ErrDashToken) }) t.Run("append with wrong element type fails", func(t *testing.T) { doc := map[string]any{"arr": []int{1, 2}} p, err := New("/arr/-") require.NoError(t, err) _, err = p.Set(doc, "not-an-int") require.Error(t, err) }) } // dashSetter captures whatever token JSONSet receives, including "-". type dashSetter struct { key string value any } func (d *dashSetter) JSONSet(key string, value any) error { d.key = key d.value = value return nil } func TestDashToken_JSONSetableReceivesRawDash(t *testing.T) { t.Parallel() // When the terminal parent implements JSONSetable, the dash token is // passed through verbatim. Semantics are the user type's responsibility. ds := &dashSetter{} p, err := New("/-") require.NoError(t, err) _, err = p.Set(ds, 42) require.NoError(t, err) assert.Equal(t, "-", ds.key) assert.Equal(t, 42, ds.value) } func TestDashToken_RoundTrip(t *testing.T) { t.Parallel() p, err := New("/a/-") require.NoError(t, err) assert.Equal(t, "/a/-", p.String()) assert.Equal(t, []string{"a", "-"}, p.DecodedTokens()) } func TestDashToken_WrappedErrors(t *testing.T) { t.Parallel() // Ensure errors.Is works through both wraps. p, _ := New("/arr/-") doc := map[string]any{"arr": []any{}} _, _, err := p.Get(doc) require.Error(t, err) assert.True(t, errors.Is(err, ErrDashToken)) assert.True(t, errors.Is(err, ErrPointer)) } go-openapi-jsonpointer-d29978d/docs/000077500000000000000000000000001517066760400174405ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/docs/.gitkeep000066400000000000000000000000001517066760400210570ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/errors.go000066400000000000000000000042641517066760400203610ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import "fmt" type pointerError string func (e pointerError) Error() string { return string(e) } const ( // ErrPointer is a sentinel error raised by all errors from this package. ErrPointer pointerError = "JSON pointer error" // ErrInvalidStart states that a JSON pointer must start with a separator ("/"). ErrInvalidStart pointerError = `JSON pointer must be empty or start with a "` + pointerSeparator + `"` // ErrUnsupportedValueType indicates that a value of the wrong type is being set. ErrUnsupportedValueType pointerError = "only structs, pointers, maps and slices are supported for setting values" // ErrDashToken indicates use of the RFC 6901 "-" reference token // in a context where it cannot be resolved. // // Per RFC 6901 §4 the "-" token refers to the (nonexistent) element // after the last array element. It may only be used as the terminal // token of a [Pointer.Set] against a slice, where it means "append". // Any other use (get, offset, intermediate traversal, non-slice target) // is an error condition that wraps this sentinel. ErrDashToken pointerError = `the "-" array token cannot be resolved here` //nolint:gosec // G101 false positive: this is a JSON Pointer reference token, not a credential. ) const dashToken = "-" func errNoKey(key string) error { return fmt.Errorf("object has no key %q: %w", key, ErrPointer) } func errOutOfBounds(length, idx int) error { return fmt.Errorf("index out of bounds array[0,%d] index '%d': %w", length-1, idx, ErrPointer) } func errInvalidReference(token string) error { return fmt.Errorf("invalid token reference %q: %w", token, ErrPointer) } func errDashOnGet() error { return fmt.Errorf("cannot resolve %q token on get: %w: %w", dashToken, ErrDashToken, ErrPointer) } func errDashIntermediate() error { return fmt.Errorf("the %q token may only appear as the terminal token of a pointer: %w: %w", dashToken, ErrDashToken, ErrPointer) } func errDashOnOffset() error { return fmt.Errorf("cannot compute offset for %q token (nonexistent element): %w: %w", dashToken, ErrDashToken, ErrPointer) } go-openapi-jsonpointer-d29978d/examples_test.go000066400000000000000000000117471517066760400217260ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "encoding/json" "errors" "fmt" "github.com/go-openapi/swag/jsonname" ) var ErrExampleStruct = errors.New("example error") type exampleDocument struct { Foo []string `json:"foo"` } func ExampleNew() { empty, err := New("") if err != nil { fmt.Println(err) return } fmt.Printf("empty pointer: %q\n", empty.String()) key, err := New("/foo") if err != nil { fmt.Println(err) return } fmt.Printf("pointer to object key: %q\n", key.String()) elem, err := New("/foo/1") if err != nil { fmt.Println(err) return } fmt.Printf("pointer to array element: %q\n", elem.String()) escaped0, err := New("/foo~0") if err != nil { fmt.Println(err) return } // key contains "~" fmt.Printf("pointer to key %q: %q\n", Unescape("foo~0"), escaped0.String()) escaped1, err := New("/foo~1") if err != nil { fmt.Println(err) return } // key contains "/" fmt.Printf("pointer to key %q: %q\n", Unescape("foo~1"), escaped1.String()) // output: // empty pointer: "" // pointer to object key: "/foo" // pointer to array element: "/foo/1" // pointer to key "foo~": "/foo~0" // pointer to key "foo/": "/foo~1" } func ExamplePointer_Get() { var doc exampleDocument if err := json.Unmarshal(testDocumentJSONBytes, &doc); err != nil { // populates doc fmt.Println(err) return } pointer, err := New("/foo/1") if err != nil { fmt.Println(err) return } value, kind, err := pointer.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf( "value: %q\nkind: %v\n", value, kind, ) // Output: // value: "baz" // kind: string } func ExamplePointer_Set() { var doc exampleDocument if err := json.Unmarshal(testDocumentJSONBytes, &doc); err != nil { // populates doc fmt.Println(err) return } pointer, err := New("/foo/1") if err != nil { fmt.Println(err) return } result, err := pointer.Set(&doc, "hey my") if err != nil { fmt.Println(err) return } fmt.Printf("result: %#v\n", result) fmt.Printf("doc: %#v\n", doc) // Output: // result: &jsonpointer.exampleDocument{Foo:[]string{"bar", "hey my"}} // doc: jsonpointer.exampleDocument{Foo:[]string{"bar", "hey my"}} } // ExamplePointer_Set_append demonstrates the RFC 6901 "-" token as an // append operation on a slice. On nested slices reached through an // addressable parent (map entry, pointer to struct, ...), the append is // performed in place and the returned document is the same reference. func ExamplePointer_Set_append() { doc := map[string]any{"foo": []any{"bar"}} pointer, err := New("/foo/-") if err != nil { fmt.Println(err) return } if _, err := pointer.Set(doc, "baz"); err != nil { fmt.Println(err) return } fmt.Printf("doc: %v\n", doc["foo"]) // Output: // doc: [bar baz] } // ExamplePointer_Set_appendTopLevelSlice shows the one case where the // returned document is load-bearing: appending to a top-level slice // passed by value. The library cannot rebind the slice header in the // caller's variable, so callers must use the returned document (or pass // *[]T to get in-place rebind). func ExamplePointer_Set_appendTopLevelSlice() { doc := []int{1, 2} pointer, err := New("/-") if err != nil { fmt.Println(err) return } out, err := pointer.Set(doc, 3) if err != nil { fmt.Println(err) return } fmt.Printf("original: %v\n", doc) fmt.Printf("returned: %v\n", out) // Output: // original: [1 2] // returned: [1 2 3] } // ExampleUseGoNameProvider contrasts the two [NameProvider] implementations // shipped by [github.com/go-openapi/swag/jsonname]: // // - the default provider requires a `json` struct tag to expose a field; // - the Go-name provider follows encoding/json conventions and accepts // exported untagged fields and promoted embedded fields as well. func ExampleUseGoNameProvider() { type Embedded struct { Nested string // untagged: promoted only by the Go-name provider } type Doc struct { Embedded // untagged embedded: promoted only by the Go-name provider Tagged string `json:"tagged"` Untagged string // no tag: visible only to the Go-name provider } doc := Doc{ Embedded: Embedded{Nested: "promoted"}, Tagged: "hit", Untagged: "hidden-by-default", } for _, path := range []string{"/tagged", "/Untagged", "/Nested"} { p, err := New(path) if err != nil { fmt.Println(err) return } // Default provider: only the tagged field resolves. defV, _, defErr := p.Get(doc) // Go-name provider: untagged and promoted fields resolve too. goV, _, goErr := p.Get(doc, WithNameProvider(jsonname.NewGoNameProvider())) fmt.Printf("%s -> default=%v (err=%v) | goname=%v (err=%v)\n", path, defV, defErr != nil, goV, goErr != nil) } // Output: // /tagged -> default=hit (err=false) | goname=hit (err=false) // /Untagged -> default= (err=true) | goname=hidden-by-default (err=false) // /Nested -> default= (err=true) | goname=promoted (err=false) } go-openapi-jsonpointer-d29978d/fuzz_test.go000066400000000000000000000013631517066760400210770ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "iter" "slices" "strings" "testing" "github.com/go-openapi/testify/v2/require" ) func FuzzParse(f *testing.F) { // initial seed cumulated := make([]string, 0, 100) for generator := range generators() { f.Add(generator) cumulated = append(cumulated, generator) f.Add(strings.Join(cumulated, "")) } f.Fuzz(func(t *testing.T, input string) { require.NotPanics(t, func() { _, _ = New(input) }) }) } func generators() iter.Seq[string] { return slices.Values([]string{ `a`, ``, `/`, `/`, `/a~1b`, `/a~1b`, `/c%d`, `/e^f`, `/g|h`, `/i\j`, `/k"l`, `/ `, `/m~0n`, `/foo`, `/0`, }) } go-openapi-jsonpointer-d29978d/go.mod000066400000000000000000000002271517066760400176170ustar00rootroot00000000000000module github.com/go-openapi/jsonpointer require ( github.com/go-openapi/swag/jsonname v0.26.0 github.com/go-openapi/testify/v2 v2.4.2 ) go 1.25.0 go-openapi-jsonpointer-d29978d/go.sum000066400000000000000000000005661517066760400176520ustar00rootroot00000000000000github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= go-openapi-jsonpointer-d29978d/iface_example_test.go000066400000000000000000000053561517066760400226710ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer_test import ( "fmt" "github.com/go-openapi/jsonpointer" ) var ( _ jsonpointer.JSONPointable = CustomDoc{} _ jsonpointer.JSONSetable = &CustomDoc{} ) // CustomDoc accepts 2 preset properties "propA" and "propB", plus any number of extra properties. // // All values are strings. type CustomDoc struct { a string b string c map[string]string } // JSONLookup implements [jsonpointer.JSONPointable]. func (d CustomDoc) JSONLookup(key string) (any, error) { switch key { case "propA": return d.a, nil case "propB": return d.b, nil default: if len(d.c) == 0 { return nil, fmt.Errorf("key %q not found: %w", key, ErrExampleIface) } extra, ok := d.c[key] if !ok { return nil, fmt.Errorf("key %q not found: %w", key, ErrExampleIface) } return extra, nil } } // JSONSet implements [jsonpointer.JSONSetable]. func (d *CustomDoc) JSONSet(key string, value any) error { asString, ok := value.(string) if !ok { return fmt.Errorf("a CustomDoc only access strings as values, but got %T: %w", value, ErrExampleIface) } switch key { case "propA": d.a = asString return nil case "propB": d.b = asString return nil default: if len(d.c) == 0 { d.c = make(map[string]string) } d.c[key] = asString return nil } } func Example_iface() { doc := CustomDoc{ a: "initial value for a", b: "initial value for b", // no extra values } pointerA, err := jsonpointer.New("/propA") if err != nil { fmt.Println(err) return } // get the initial value for a propA, kind, err := pointerA.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf("propA (%v): %v\n", kind, propA) pointerB, err := jsonpointer.New("/propB") if err != nil { fmt.Println(err) return } // get the initial value for b propB, kind, err := pointerB.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf("propB (%v): %v\n", kind, propB) pointerC, err := jsonpointer.New("/extra") if err != nil { fmt.Println(err) return } // not found yet _, _, err = pointerC.Get(doc) fmt.Printf("propC: %v\n", err) _, err = pointerA.Set(&doc, "new value for a") // doc is updated in place if err != nil { fmt.Println(err) return } _, err = pointerB.Set(&doc, "new value for b") if err != nil { fmt.Println(err) return } _, err = pointerC.Set(&doc, "new extra value") if err != nil { fmt.Println(err) return } fmt.Printf("updated doc: %v", doc) // output: // propA (string): initial value for a // propB (string): initial value for b // propC: key "extra" not found: example error // updated doc: {new value for a new value for b map[extra:new extra value]} } go-openapi-jsonpointer-d29978d/ifaces.go000066400000000000000000000040121517066760400202660ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import "reflect" // JSONPointable is an interface for structs to implement, // when they need to customize the json pointer process or want to avoid the use of reflection. type JSONPointable interface { // JSONLookup returns a value pointed at this (unescaped) key. JSONLookup(key string) (any, error) } // JSONSetable is an interface for structs to implement, // when they need to customize the json pointer process or want to avoid the use of reflection. // // # Handling of the RFC 6901 "-" token // // When a type implementing JSONSetable is the terminal parent of a [Pointer.Set] // call, the library passes the raw reference token to JSONSet without // interpretation. In particular, the RFC 6901 "-" token (which conventionally // means "append" for arrays, per RFC 6902) is forwarded verbatim as the key // argument. Implementations that model an array-like container are expected // to give "-" the append semantics; implementations that do not should return // an error wrapping [ErrDashToken] (or [ErrPointer]) for clarity. // // Implementations are responsible for any in-place mutation: the library does // not attempt to rebind the result of JSONSet into a parent container. type JSONSetable interface { // JSONSet sets the value pointed at the (unescaped) key. // // The key may be the RFC 6901 "-" token when the pointer targets a // slice-like member; see the interface documentation for details. JSONSet(key string, value any) error } // NameProvider knows how to resolve go struct fields into json names. // // The default provider is brought by [github.com/go-openapi/swag/jsonname.DefaultJSONNameProvider]. type NameProvider interface { // GetGoName gets the go name for a json property name GetGoName(subject any, name string) (string, bool) // GetGoNameForType gets the go name for a given type for a json property name GetGoNameForType(tpe reflect.Type, name string) (string, bool) } go-openapi-jsonpointer-d29978d/options.go000066400000000000000000000047771517066760400205510ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "sync" "github.com/go-openapi/swag/jsonname" ) // Option to tune the behavior of a JSON [Pointer]. type Option func(*options) var ( //nolint:gochecknoglobals // package level defaults are provided as a convenient, backward-compatible way to adopt options. defaultOptions = options{ provider: jsonname.DefaultJSONNameProvider, } //nolint:gochecknoglobals // guards defaultOptions against concurrent SetDefaultNameProvider / read races (testing) defaultOptionsMu sync.RWMutex ) // SetDefaultNameProvider sets the [NameProvider] as a package-level default. // // By default, the default provider is [jsonname.DefaultJSONNameProvider]. // // It is safe to call concurrently with [Pointer.Get], [Pointer.Set], // [GetForToken] and [SetForToken]. The typical usage is to call it once // at initialization time. // // A nil provider is ignored. func SetDefaultNameProvider(provider NameProvider) { if provider == nil { return } defaultOptionsMu.Lock() defer defaultOptionsMu.Unlock() defaultOptions.provider = provider } // UseGoNameProvider sets the [NameProvider] as a package-level default // to the alternative provider [jsonname.GoNameProvider], that covers a few areas // not supported by the default name provider. // // This implementation supports untagged exported fields and embedded types in go struct. // It follows strictly the behavior of the JSON standard library regarding field naming conventions. // // It is safe to call concurrently with [Pointer.Get], [Pointer.Set], // [GetForToken] and [SetForToken]. The typical usage is to call it once // at initialization time. func UseGoNameProvider() { SetDefaultNameProvider(jsonname.NewGoNameProvider()) } // DefaultNameProvider returns the current package-level [NameProvider]. func DefaultNameProvider() NameProvider { //nolint:ireturn // returning the interface is the point — callers pick their own implementation. defaultOptionsMu.RLock() defer defaultOptionsMu.RUnlock() return defaultOptions.provider } // WithNameProvider injects a custom [NameProvider] to resolve json names from go struct types. func WithNameProvider(provider NameProvider) Option { return func(o *options) { o.provider = provider } } type options struct { provider NameProvider } func optionsWithDefaults(opts []Option) options { var o options o.provider = DefaultNameProvider() for _, apply := range opts { apply(&o) } return o } go-openapi-jsonpointer-d29978d/options_test.go000066400000000000000000000077111517066760400215770ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "reflect" "sync" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) // stubNameProvider is a NameProvider that maps JSON names to Go field names // via a fixed dictionary. It lets tests observe which provider was used by // the resolver without relying on the default reflection-based behavior. type stubNameProvider struct { mu sync.Mutex mapping map[string]string lookups []string forTypes []string } func (s *stubNameProvider) GetGoName(_ any, name string) (string, bool) { s.record(name, false) goName, ok := s.mapping[name] return goName, ok } func (s *stubNameProvider) GetGoNameForType(_ reflect.Type, name string) (string, bool) { s.record(name, true) goName, ok := s.mapping[name] return goName, ok } func (s *stubNameProvider) record(name string, forType bool) { s.mu.Lock() defer s.mu.Unlock() if forType { s.forTypes = append(s.forTypes, name) return } s.lookups = append(s.lookups, name) } type optionStruct struct { // intentional: the JSON name "renamed" is deliberately not a valid // struct tag so that only a custom provider can resolve it. Field string } func TestWithNameProvider_overridesDefault(t *testing.T) { t.Parallel() stub := &stubNameProvider{mapping: map[string]string{"renamed": "Field"}} doc := optionStruct{Field: "hello"} p, err := New("/renamed") require.NoError(t, err) v, _, err := p.Get(doc, WithNameProvider(stub)) require.NoError(t, err) assert.Equal(t, "hello", v) stub.mu.Lock() defer stub.mu.Unlock() assert.Contains(t, stub.forTypes, "renamed", "custom provider must be consulted") } func TestWithNameProvider_setRoutesThroughProvider(t *testing.T) { t.Parallel() stub := &stubNameProvider{mapping: map[string]string{"renamed": "Field"}} doc := &optionStruct{Field: "before"} p, err := New("/renamed") require.NoError(t, err) _, err = p.Set(doc, "after", WithNameProvider(stub)) require.NoError(t, err) assert.Equal(t, "after", doc.Field) } func TestSetDefaultNameProvider_roundTrip(t *testing.T) { // Not Parallel: mutates package state. original := DefaultNameProvider() t.Cleanup(func() { SetDefaultNameProvider(original) }) stub := &stubNameProvider{mapping: map[string]string{"renamed": "Field"}} SetDefaultNameProvider(stub) assert.Same(t, stub, DefaultNameProvider()) doc := optionStruct{Field: "hello"} p, err := New("/renamed") require.NoError(t, err) v, _, err := p.Get(doc) require.NoError(t, err) assert.Equal(t, "hello", v) } func TestSetDefaultNameProvider_nilIgnored(t *testing.T) { // Not Parallel: mutates package state. original := DefaultNameProvider() t.Cleanup(func() { SetDefaultNameProvider(original) }) SetDefaultNameProvider(nil) assert.Same(t, original, DefaultNameProvider(), "nil must be a no-op") } func TestUseGoNameProvider_resolvesUntaggedFields(t *testing.T) { // Not Parallel: mutates package state. original := DefaultNameProvider() t.Cleanup(func() { SetDefaultNameProvider(original) }) // optionStruct.Field has no json tag; the default provider can't resolve it, // but the Go-name provider follows encoding/json conventions and can. doc := optionStruct{Field: "hello"} p, err := New("/Field") require.NoError(t, err) _, _, err = p.Get(doc) require.Error(t, err, "default provider should not resolve untagged fields") UseGoNameProvider() v, _, err := p.Get(doc) require.NoError(t, err) assert.Equal(t, "hello", v) } func TestDefaultNameProvider_reachesGetForToken(t *testing.T) { // Not Parallel: mutates package state. original := DefaultNameProvider() t.Cleanup(func() { SetDefaultNameProvider(original) }) stub := &stubNameProvider{mapping: map[string]string{"renamed": "Field"}} SetDefaultNameProvider(stub) doc := optionStruct{Field: "hello"} v, _, err := GetForToken(doc, "renamed") require.NoError(t, err) assert.Equal(t, "hello", v) } go-openapi-jsonpointer-d29978d/pointer.go000066400000000000000000000543331517066760400205270ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package jsonpointer provides a golang implementation for json pointers. package jsonpointer import ( "encoding/json" "errors" "fmt" "reflect" "strconv" "strings" ) const ( emptyPointer = `` pointerSeparator = `/` ) // Pointer is a representation of a json pointer. // // Use [Pointer.Get] to retrieve a value or [Pointer.Set] to set a value. // // It works with any go type interpreted as a JSON document, which means: // // - if a type implements [JSONPointable], its [JSONPointable.JSONLookup] method is used to resolve [Pointer.Get] // - if a type implements [JSONSetable], its [JSONSetable.JSONSet] method is used to resolve [Pointer.Set] // - a go map[K]V is interpreted as an object, with type K assignable to a string // - a go slice []T is interpreted as an array // - a go struct is interpreted as an object, with exported fields interpreted as keys // - promoted fields from an embedded struct are traversed // - scalars (e.g. int, float64 ...), channels, functions and go arrays cannot be traversed // // For struct s resolved by reflection, key mappings honor the conventional struct tag `json`. // // Fields that do not specify a `json` tag, or specify an empty one, or are tagged as `json:"-"` are ignored. // // # Limitations // // - Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored. // - anonymous fields are not traversed if untagged type Pointer struct { referenceTokens []string } // New creates a new json pointer from its string representation. func New(jsonPointerString string) (Pointer, error) { var p Pointer err := p.parse(jsonPointerString) return p, err } // Get uses the pointer to retrieve a value from a JSON document. // // It returns the value with its type as a [reflect.Kind] or an error. func (p *Pointer) Get(document any, opts ...Option) (any, reflect.Kind, error) { o := optionsWithDefaults(opts) return p.get(document, o.provider) } // Set uses the pointer to set a value from a data type // that represent a JSON document. // // # Mutation contract // // Set mutates the provided document in place whenever Go's type system allows // it: when document is a map, a pointer, or when the targeted value is reached // through an addressable ancestor (e.g. a struct field traversed via a pointer, // a slice element). Callers that rely on this in-place behavior may continue // to ignore the returned document. // // The returned document is only load-bearing when Set cannot mutate in place. // This happens in one specific case: appending to a top-level slice passed by // value (e.g. document of type []T rather than *[]T) via the RFC 6901 "-" // terminal token. reflect.Append produces a new slice header that the library // cannot rebind into the caller's variable; the updated document is returned // instead. Pass *[]T if you want in-place rebind for that case as well. // // See [ErrDashToken] for the semantics of the "-" token. func (p *Pointer) Set(document any, value any, opts ...Option) (any, error) { o := optionsWithDefaults(opts) return p.set(document, value, o.provider) } // DecodedTokens returns the decoded (unescaped) tokens of this JSON pointer. func (p *Pointer) DecodedTokens() []string { result := make([]string, 0, len(p.referenceTokens)) for _, token := range p.referenceTokens { result = append(result, Unescape(token)) } return result } // IsEmpty returns true if this is an empty json pointer. // // This indicates that it points to the root document. func (p *Pointer) IsEmpty() bool { return len(p.referenceTokens) == 0 } // String representation of a pointer. func (p *Pointer) String() string { if len(p.referenceTokens) == 0 { return emptyPointer } return pointerSeparator + strings.Join(p.referenceTokens, pointerSeparator) } // Offset returns the byte offset, in the raw JSON text of document, of the // location referenced by this pointer's terminal token. // // Unlike [Pointer.Get] and [Pointer.Set], which operate on a decoded Go value, // Offset operates directly on the textual JSON source. It drives an // [encoding/json.Decoder] over the string and stops at the terminal token, // returning the position at which the decoder was about to read that token. // // It is primarily intended for tooling that needs to map a pointer back to a // region of the original source: reporting line/column for validation or // parse diagnostics, extracting a sub-document by slicing the raw bytes, or // highlighting the referenced span in an editor. // // # Offset semantics // // The meaning of the returned offset depends on whether the terminal token // addresses an object property or an array element: // // - Object property: the offset points to the first byte of the key (its // opening quote character), not to the associated value. For example, // pointer "/foo/bar" against {"foo": {"bar": 21}} returns 9, the index of // the opening quote of "bar". // - Array element: the offset points to the first byte of the value at that // index. For example, pointer "/0/1" against [[1,2], [3,4]] returns 4, // the index of the digit 2. // // # Errors // // Offset returns an error in any of these cases: // // - document is not syntactically valid JSON; // - the structure of document does not match the pointer (e.g. traversing // into a scalar, or a token that is neither a valid key nor a valid // numeric index); // - a referenced key or index does not exist in document; // - the pointer's terminal token is the RFC 6901 "-" array token, which // designates a nonexistent element and therefore has no offset in the // source. The returned error wraps [ErrDashToken]. // // All errors wrap [ErrPointer]. 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: %w", tk, ErrPointer) } default: return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer) } } return skipJSONSeparator(document, offset), nil } // skipJSONSeparator advances offset past trailing JSON whitespace and at most // one value separator (comma) in document, so the result points at the first // byte of the next JSON token. // // The streaming decoder's InputOffset sits right after the most recently // consumed token, which between values is the comma (or whitespace) — not // the following token. Normalizing here keeps Offset's contract uniform: // for both object keys and array elements, and regardless of position within // the parent container, the returned offset always points at the first byte // of the addressed token. func skipJSONSeparator(document string, offset int64) int64 { n := int64(len(document)) for offset < n && isJSONWhitespace(document[offset]) { offset++ } if offset < n && document[offset] == ',' { offset++ } for offset < n && isJSONWhitespace(document[offset]) { offset++ } return offset } func isJSONWhitespace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' } // "Constructor", parses the given string JSON pointer. func (p *Pointer) parse(jsonPointerString string) error { if jsonPointerString == emptyPointer { return nil } if !strings.HasPrefix(jsonPointerString, pointerSeparator) { // non empty pointer must start with "/" return errors.Join(ErrInvalidStart, ErrPointer) } referenceTokens := strings.Split(jsonPointerString, pointerSeparator) p.referenceTokens = append(p.referenceTokens, referenceTokens[1:]...) return nil } func (p *Pointer) get(node any, nameProvider NameProvider) (any, reflect.Kind, error) { if nameProvider == nil { nameProvider = defaultOptions.provider } kind := reflect.Invalid // full document when empty if len(p.referenceTokens) == 0 { return node, kind, nil } for _, token := range p.referenceTokens { decodedToken := Unescape(token) r, knd, err := getSingleImpl(node, decodedToken, nameProvider) if err != nil { return nil, knd, err } node = r } rValue := reflect.ValueOf(node) kind = rValue.Kind() return node, kind, nil } func (p *Pointer) set(node, data any, nameProvider NameProvider) (any, error) { knd := reflect.ValueOf(node).Kind() if knd != reflect.Pointer && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array { return node, errors.Join( fmt.Errorf("unexpected type: %T", node), //nolint:err113 // err wrapping is carried out by errors.Join, not fmt.Errorf. ErrUnsupportedValueType, ErrPointer, ) } // full document when empty if len(p.referenceTokens) == 0 { return node, nil } if nameProvider == nil { nameProvider = defaultOptions.provider } return p.setAt(node, p.referenceTokens, data, nameProvider) } // setAt recursively walks the token list, setting the data at the terminal // token and rebinding any new child reference (e.g. a slice header returned // by an "-" append) into its parent on the way back up. // // Returning the (possibly new) node at each level is what makes append work // at any depth without requiring the caller to pass a pointer to the // containing slice: the new slice header propagates up and each parent // rebinds it via the appropriate kind-specific setter. func (p *Pointer) setAt(node any, tokens []string, data any, nameProvider NameProvider) (any, error) { decodedToken := Unescape(tokens[0]) if len(tokens) == 1 { return setSingleImpl(node, data, decodedToken, nameProvider) } child, err := p.resolveNodeForToken(node, decodedToken, nameProvider) if err != nil { return node, err } newChild, err := p.setAt(child, tokens[1:], data, nameProvider) if err != nil { return node, err } return rebindChild(node, decodedToken, newChild, nameProvider) } // rebindChild writes newChild back into node at decodedToken. // // For cases where the child was already mutated in place (pointer aliasing, // addressable slice elements) the rebind is a safe no-op. For cases where // the child was returned by value (map entries holding a slice, slices // reached through a non-addressable ancestor), the rebind propagates the // new value into the parent. // // Parents implementing [JSONPointable] are left alone: they took ownership // of the child via JSONLookup and did not opt into a JSONSet-based rebind // on intermediate tokens. func rebindChild(node any, decodedToken string, newChild any, nameProvider NameProvider) (any, error) { if _, ok := node.(JSONPointable); ok { return node, nil } rValue := reflect.Indirect(reflect.ValueOf(node)) switch rValue.Kind() { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return node, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } fld := rValue.FieldByName(nm) if !fld.CanSet() { return node, nil } assignReflectValue(fld, newChild) return node, nil case reflect.Map: rValue.SetMapIndex(reflect.ValueOf(decodedToken), reflect.ValueOf(newChild)) return node, nil case reflect.Slice: if decodedToken == dashToken { return node, errDashIntermediate() } idx, err := strconv.Atoi(decodedToken) if err != nil { return node, errors.Join(err, ErrPointer) } elem := rValue.Index(idx) if !elem.CanSet() { return node, nil } assignReflectValue(elem, newChild) return node, nil default: return node, errInvalidReference(decodedToken) } } // assignReflectValue assigns src into dst, unwrapping a pointer when dst // expects the pointee type. This tolerates the pointer-wrapping performed // by [typeFromValue] for addressable fields. func assignReflectValue(dst reflect.Value, src any) { nv := reflect.ValueOf(src) if !nv.IsValid() { return } if nv.Type().AssignableTo(dst.Type()) { dst.Set(nv) return } if nv.Kind() == reflect.Pointer && nv.Elem().Type().AssignableTo(dst.Type()) { dst.Set(nv.Elem()) } } func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider NameProvider) (next any, err error) { // check for nil during traversal if isNil(node) { return nil, fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer) } pointable, ok := node.(JSONPointable) if ok { r, err := pointable.JSONLookup(decodedToken) if err != nil { return nil, err } fld := reflect.ValueOf(r) if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Pointer { return fld.Addr().Interface(), nil } return r, nil } rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() switch kind { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return nil, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } return typeFromValue(rValue.FieldByName(nm)), nil case reflect.Map: kv := reflect.ValueOf(decodedToken) mv := rValue.MapIndex(kv) if !mv.IsValid() { return nil, errNoKey(decodedToken) } return typeFromValue(mv), nil case reflect.Slice: if decodedToken == dashToken { return nil, errDashIntermediate() } tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return nil, errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return nil, errOutOfBounds(sLength, tokenIndex) } return typeFromValue(rValue.Index(tokenIndex)), nil default: return nil, errInvalidReference(decodedToken) } } func isNil(input any) bool { if input == nil { return true } kind := reflect.TypeOf(input).Kind() switch kind { case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan: return reflect.ValueOf(input).IsNil() default: return false } } func typeFromValue(v reflect.Value) any { if v.CanAddr() && v.Kind() != reflect.Interface && v.Kind() != reflect.Map && v.Kind() != reflect.Slice && v.Kind() != reflect.Pointer { return v.Addr().Interface() } return v.Interface() } // GetForToken gets a value for a json pointer token 1 level deep. func GetForToken(document any, decodedToken string, opts ...Option) (any, reflect.Kind, error) { o := optionsWithDefaults(opts) return getSingleImpl(document, decodedToken, o.provider) } // SetForToken sets a value for a json pointer token 1 level deep. // // See [Pointer.Set] for the mutation contract, in particular the handling of // the RFC 6901 "-" token on slices. func SetForToken(document any, decodedToken string, value any, opts ...Option) (any, error) { o := optionsWithDefaults(opts) return setSingleImpl(document, value, decodedToken, o.provider) } func getSingleImpl(node any, decodedToken string, nameProvider 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 no field %q: %w", decodedToken, ErrPointer) } 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 { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return nil, kind, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } fld := rValue.FieldByName(nm) return fld.Interface(), kind, nil case reflect.Map: kv := reflect.ValueOf(decodedToken) mv := rValue.MapIndex(kv) if mv.IsValid() { return mv.Interface(), kind, nil } return nil, kind, errNoKey(decodedToken) case reflect.Slice: if decodedToken == dashToken { return nil, kind, errDashOnGet() } tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return nil, kind, errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return nil, kind, errOutOfBounds(sLength, tokenIndex) } elem := rValue.Index(tokenIndex) return elem.Interface(), kind, nil default: return nil, kind, errInvalidReference(decodedToken) } } func setSingleImpl(node, data any, decodedToken string, nameProvider NameProvider) (any, error) { // check for nil to prevent panic when calling rValue.Type() if isNil(node) { return node, fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer) } if ns, ok := node.(JSONSetable); ok { return node, ns.JSONSet(decodedToken, data) } rValue := reflect.Indirect(reflect.ValueOf(node)) switch rValue.Kind() { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return node, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } fld := rValue.FieldByName(nm) if !fld.CanSet() { return node, fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer) } value := reflect.ValueOf(data) valueType := value.Type() assignedType := fld.Type() if !valueType.AssignableTo(assignedType) { return node, fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer) } fld.Set(value) return node, nil case reflect.Map: kv := reflect.ValueOf(decodedToken) rValue.SetMapIndex(kv, reflect.ValueOf(data)) return node, nil case reflect.Slice: if decodedToken == dashToken { // RFC 6901 §4 / RFC 6902 append semantics: terminal "-" appends // the value to the slice. We rebind in place when the slice is // reachable via an addressable ancestor; otherwise we return the // new slice header for the parent (or the public Set) to rebind. value := reflect.ValueOf(data) elemType := rValue.Type().Elem() if !value.Type().AssignableTo(elemType) { return node, fmt.Errorf("can't append value of type %T to slice of %v: %w", data, elemType, ErrPointer) } newSlice := reflect.Append(rValue, value) if rValue.CanSet() { rValue.Set(newSlice) return node, nil } return newSlice.Interface(), nil } tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return node, errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return node, errOutOfBounds(sLength, tokenIndex) } elem := rValue.Index(tokenIndex) if !elem.CanSet() { return node, fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer) } value := reflect.ValueOf(data) valueType := value.Type() assignedType := elem.Type() if !valueType.AssignableTo(assignedType) { return node, fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer) } elem.Set(value) return node, nil default: return node, errInvalidReference(decodedToken) } } 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 } key, ok := tk.(string) if !ok { return 0, fmt.Errorf("invalid key token %#v: %w", tk, ErrPointer) } if key == decodedToken { return offset, nil } // Consume the associated value. Scalars are fully read by a single // Token() call; composite values must be drained. 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 } } } } return 0, fmt.Errorf("token reference %q not found: %w", decodedToken, ErrPointer) } func offsetSingleArray(dec *json.Decoder, decodedToken string) (int64, error) { if decodedToken == dashToken { return 0, errDashOnOffset() } idx, err := strconv.Atoi(decodedToken) if err != nil { return 0, fmt.Errorf("token reference %q is not a number: %w: %w", decodedToken, err, ErrPointer) } 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: %w", decodedToken, ErrPointer) } 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 } // JSON pointer encoding: // ~0 => ~ // ~1 => / // ... and vice versa const ( encRefTok0 = `~0` encRefTok1 = `~1` decRefTok0 = `~` decRefTok1 = `/` ) var ( encRefTokReplacer = strings.NewReplacer(encRefTok1, decRefTok1, encRefTok0, decRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global decRefTokReplacer = strings.NewReplacer(decRefTok1, encRefTok1, decRefTok0, encRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global ) // Unescape unescapes a json pointer reference token string to the original representation. func Unescape(token string) string { return encRefTokReplacer.Replace(token) } // Escape escapes a pointer reference token string. // // The JSONPointer specification defines "/" as a separator and "~" as an escape prefix. // // Keys containing such characters are escaped with the following rules: // // - "~" is escaped as "~0" // - "/" is escaped as "~1" func Escape(token string) string { return decRefTokReplacer.Replace(token) } go-openapi-jsonpointer-d29978d/pointer_test.go000066400000000000000000000736631517066760400215750ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( "encoding/json" "fmt" "reflect" "strconv" "testing" "github.com/go-openapi/swag/jsonname" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func TestEscaping(t *testing.T) { t.Parallel() t.Run("escaped pointer strings against test document", func(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 { t.Run("should create a JSONPointer", func(t *testing.T) { p, err := New(ins[i]) require.NoError(t, err, "input: %v", ins[i]) t.Run("should get JSONPointer from document", func(t *testing.T) { result, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err, "input: %v", ins[i]) assert.InDeltaf(t, outs[i], result, 1e-6, "input: %v", ins[i]) }) }) } }) t.Run("special escapes", func(t *testing.T) { t.Parallel() t.Run("with escape then unescape", func(t *testing.T) { const original = "a/" t.Run("unescaping an escaped string should yield the original", func(t *testing.T) { esc := Escape(original) assert.EqualT(t, "a~1", esc) unesc := Unescape(esc) assert.EqualT(t, original, unesc) }) }) t.Run("with multiple escapes", func(t *testing.T) { unesc := Unescape("~01") assert.EqualT(t, "~1", unesc) assert.EqualT(t, "~01", Escape(unesc)) const ( original = "~/" escaped = "~0~1" ) assert.EqualT(t, escaped, Escape(original)) assert.EqualT(t, original, Unescape(escaped)) }) t.Run("with escaped characters in pointer", func(t *testing.T) { t.Run("escaped ~", func(t *testing.T) { s := Escape("m~n") assert.EqualT(t, "m~0n", s) }) t.Run("escaped /", func(t *testing.T) { s := Escape("m/n") assert.EqualT(t, "m~1n", s) }) }) }) } func TestFullDocument(t *testing.T) { t.Parallel() t.Run("with empty pointer", func(t *testing.T) { const in = `` p, err := New(in) require.NoErrorf(t, err, "New(%v) error %v", in, err) t.Run("should resolve full doc", func(t *testing.T) { result, _, err := p.Get(testDocumentJSON(t)) require.NoErrorf(t, err, "Get(%v) error %v", in, err) asMap, ok := result.(map[string]any) require.TrueT(t, ok) require.Lenf(t, asMap, testDocumentNBItems(), "Get(%v) = %v, expect full document", in, result) }) t.Run("should resolve full doc, with nil name provider", func(t *testing.T) { result, _, err := p.get(testDocumentJSON(t), nil) require.NoErrorf(t, err, "Get(%v) error %v", in, err) asMap, ok := result.(map[string]any) require.TrueT(t, ok) require.Lenf(t, asMap, testDocumentNBItems(), "Get(%v) = %v, expect full document", in, result) t.Run("should set value in doc, with nil name provider", func(t *testing.T) { setter, err := New("/foo/0") require.NoErrorf(t, err, "New(%v) error %v", in, err) const value = "hey" _, err = setter.set(asMap, value, nil) require.NoError(t, err) foos, ok := asMap["foo"] require.TrueT(t, ok) asArray, ok := foos.([]any) require.TrueT(t, ok) require.Len(t, asArray, 2) foo := asArray[0] bar, ok := foo.(string) require.TrueT(t, ok) require.EqualT(t, value, bar) }) }) }) } func TestDecodedTokens(t *testing.T) { t.Parallel() p, err := New("/obj/a~1b") require.NoError(t, err) assert.Equal(t, []string{"obj", "a/b"}, p.DecodedTokens()) } func TestIsEmpty(t *testing.T) { t.Parallel() t.Run("with empty pointer", func(t *testing.T) { p, err := New("") require.NoError(t, err) assert.TrueT(t, p.IsEmpty()) }) t.Run("with non-empty pointer", func(t *testing.T) { p, err := New("/obj") require.NoError(t, err) assert.FalseT(t, p.IsEmpty()) }) } func TestGetSingle(t *testing.T) { t.Parallel() const key = "obj" t.Run("should create a new JSON pointer", func(t *testing.T) { const in = "/" + key _, err := New(in) require.NoError(t, err) }) t.Run(fmt.Sprintf("should find token %q in JSON", key), func(t *testing.T) { result, _, err := GetForToken(testDocumentJSON(t), key) require.NoError(t, err) assert.Len(t, result, testNodeObjNBItems()) }) t.Run(fmt.Sprintf("should find token %q in type alias interface", key), func(t *testing.T) { type alias any var in alias = testDocumentJSON(t) result, _, err := GetForToken(in, key) require.NoError(t, err) assert.Len(t, result, testNodeObjNBItems()) }) t.Run(fmt.Sprintf("should find token %q in pointer to interface", key), func(t *testing.T) { in := testDocumentJSON(t) result, _, err := GetForToken(&in, key) 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(t), "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(t), "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, key) require.Error(t, err) assert.Nil(t, result) }) t.Run("should not find token in nil interface", func(t *testing.T) { var in any result, _, err := GetForToken(in, key) require.Error(t, err) assert.Nil(t, result) }) } type pointableImpl struct { a string } func (p pointableImpl) JSONLookup(token string) (any, error) { if token == "some" { return p.a, nil } return nil, fmt.Errorf("object has no field %q: %w", token, ErrPointer) } type pointableMap map[string]string func (p pointableMap) JSONLookup(token string) (any, error) { if token == "swap" { return p["swapped"], nil } v, ok := p[token] if ok { return v, nil } return nil, fmt.Errorf("object has no key %q: %w", token, ErrPointer) } func TestPointableInterface(t *testing.T) { t.Parallel() t.Run("with pointable type", func(t *testing.T) { p := &pointableImpl{"hello"} result, _, err := GetForToken(p, "some") require.NoError(t, err) assert.Equal(t, p.a, result) result, _, err = GetForToken(p, "something") require.Error(t, err) assert.Nil(t, result) }) t.Run("with pointable map", func(t *testing.T) { p := pointableMap{"swapped": "hello", "a": "world"} result, _, err := GetForToken(p, "swap") require.NoError(t, err) assert.Equal(t, p["swapped"], result) result, _, err = GetForToken(p, "a") require.NoError(t, err) assert.Equal(t, p["a"], result) }) } func TestGetNode(t *testing.T) { t.Parallel() const in = `/obj` t.Run("should build pointer", func(t *testing.T) { p, err := New(in) require.NoError(t, err) t.Run("should resolve pointer against document", func(t *testing.T) { result, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err) assert.Len(t, result, testNodeObjNBItems()) }) t.Run("with aliased map", func(t *testing.T) { asMap, ok := testDocumentJSON(t).(map[string]any) require.TrueT(t, ok) alias := aliasedMap(asMap) result, _, err := p.Get(alias) require.NoError(t, err) assert.Len(t, result, testNodeObjNBItems()) }) t.Run("with struct", func(t *testing.T) { doc := testStructJSONDoc(t) expected := testStructJSONDoc(t).Obj result, _, err := p.Get(doc) require.NoError(t, err) assert.Equal(t, expected, result) }) t.Run("with pointer to struct", func(t *testing.T) { doc := testStructJSONPtr(t) expected := testStructJSONDoc(t).Obj result, _, err := p.Get(doc) require.NoError(t, err) assert.Equal(t, expected, result) }) }) } func TestArray(t *testing.T) { t.Parallel() ins := []string{`/foo/0`, `/foo/0`, `/foo/1`} outs := []string{"bar", "bar", "baz"} for i, pointer := range ins { expected := outs[i] t.Run(fmt.Sprintf("with pointer %q", pointer), func(t *testing.T) { p, err := New(pointer) require.NoError(t, err) t.Run("should resolve against struct", func(t *testing.T) { result, _, err := p.Get(testStructJSONDoc(t)) require.NoError(t, err) assert.Equal(t, expected, result) }) t.Run("should resolve against pointer to struct", func(t *testing.T) { result, _, err := p.Get(testStructJSONPtr(t)) require.NoError(t, err) assert.Equal(t, expected, result) }) t.Run("should resolve against dynamic JSON map", func(t *testing.T) { result, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err) assert.Equal(t, expected, result) }) }) } } func TestStruct(t *testing.T) { t.Parallel() t.Run("with untagged struct field", func(t *testing.T) { type Embedded struct { D int `json:"d"` } s := struct { Embedded A int `json:"a"` B int Anonymous struct { C int `json:"c"` } }{} { s.A = 1 s.B = 2 s.Anonymous.C = 3 s.D = 4 } t.Run(`should resolve field A tagged "a"`, func(t *testing.T) { pointerA, err := New("/a") require.NoError(t, err) value, kind, err := pointerA.Get(s) require.NoError(t, err) require.EqualT(t, reflect.Int, kind) require.Equal(t, 1, value) _, err = pointerA.Set(&s, 9) require.NoError(t, err) value, _, err = pointerA.Get(s) require.NoError(t, err) require.Equal(t, 9, value) }) t.Run(`should resolve embedded field D with tag`, func(t *testing.T) { pointerD, err := New("/d") require.NoError(t, err) value, kind, err := pointerD.Get(s) require.NoError(t, err) require.EqualT(t, reflect.Int, kind) require.Equal(t, 4, value) _, err = pointerD.Set(&s, 6) require.NoError(t, err) value, _, err = pointerD.Get(s) require.NoError(t, err) require.Equal(t, 6, value) }) t.Run("with known limitations", func(t *testing.T) { t.Run(`should not resolve field B without tag`, func(t *testing.T) { pointerB, err := New("/B") require.NoError(t, err) _, _, err = pointerB.Get(s) require.Error(t, err) require.ErrorContains(t, err, `has no field "B"`) _, err = pointerB.Set(&s, 8) require.Error(t, err) require.ErrorContains(t, err, `has no field "B"`) }) t.Run(`should not resolve field C with tag, but anonymous`, func(t *testing.T) { pointerC, err := New("/c") require.NoError(t, err) _, _, err = pointerC.Get(s) require.Error(t, err) require.ErrorContains(t, err, `has no field "c"`) _, err = pointerC.Set(&s, 7) require.Error(t, err) require.ErrorContains(t, err, `has no field "c"`) }) }) }) } func TestOtherThings(t *testing.T) { t.Parallel() t.Run("single string pointer should be valid", func(t *testing.T) { _, err := New("abc") require.Error(t, err) assert.EqualError(t, err, `JSON pointer must be empty or start with a "/" JSON pointer error`) }) t.Run("empty string pointer should be valid", func(t *testing.T) { p, err := New("") require.NoError(t, err) assert.Empty(t, p.String()) }) t.Run("string representation of a pointer", func(t *testing.T) { p, err := New("/obj/a") require.NoError(t, err) assert.EqualT(t, "/obj/a", p.String()) }) t.Run("out of bound array index should error", func(t *testing.T) { t.Run("with index overflow", func(t *testing.T) { p, err := New("/foo/3") require.NoError(t, err) _, _, err = p.Get(testDocumentJSON(t)) require.Error(t, err) }) t.Run("with index unerflow", func(t *testing.T) { p, err := New("/foo/-3") require.NoError(t, err) _, _, err = p.Get(testDocumentJSON(t)) require.Error(t, err) }) }) t.Run("referring to a key in an array should error", func(t *testing.T) { p, err := New("/foo/a") require.NoError(t, err) _, _, err = p.Get(testDocumentJSON(t)) require.Error(t, err) }) t.Run("referring to a non-existing key in an array should error", func(t *testing.T) { p, err := New("/notthere") require.NoError(t, err) _, _, err = p.Get(testDocumentJSON(t)) require.Error(t, err) }) t.Run("resolving pointer against an unsupported type (int) should error", func(t *testing.T) { p, err := New("/invalid") require.NoError(t, err) _, _, err = p.Get(1234) require.Error(t, err) }) t.Run("with pointer to an array index", func(t *testing.T) { for index := range 2 { p, err := New(fmt.Sprintf("/foo/%d", index)) require.NoError(t, err) v, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err) expected := extractFooKeyIndex(t, index) assert.Equal(t, expected, v) } }) } func extractFooKeyIndex(t *testing.T, index int) any { t.Helper() asMap, ok := testDocumentJSON(t).(map[string]any) require.TrueT(t, ok) // {"foo": [ ... ] } bbb, ok := asMap["foo"] require.TrueT(t, ok) asArray, ok := bbb.([]any) require.TrueT(t, ok) return asArray[index] } func TestObject(t *testing.T) { t.Parallel() 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]) require.NoError(t, err) result, _, err := p.Get(testDocumentJSON(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) result, _, err = p.Get(testStructJSONDoc(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) result, _, err = p.Get(testStructJSONPtr(t)) require.NoError(t, err) assert.InDelta(t, outs[i], result, 1e-6) } } type setJSONDoc struct { A []struct { B int `json:"b"` C int `json:"c"` } `json:"a"` D int `json:"d"` } type settableDoc struct { Coll settableColl Int settableInt } func (s settableDoc) MarshalJSON() ([]byte, error) { var res struct { A settableColl `json:"a"` D settableInt `json:"d"` } res.A = s.Coll res.D = s.Int return json.Marshal(res) } func (s *settableDoc) UnmarshalJSON(data []byte) error { var res struct { A settableColl `json:"a"` D settableInt `json:"d"` } if err := json.Unmarshal(data, &res); err != nil { return err } s.Coll = res.A s.Int = res.D return nil } // JSONLookup implements an interface to customize json pointer lookup. func (s settableDoc) JSONLookup(token string) (any, error) { switch token { case "a": return &s.Coll, nil case "d": return &s.Int, nil default: return nil, fmt.Errorf("%s is not a known field: %w", token, ErrPointer) } } // JSONLookup implements an interface to customize json pointer lookup. func (s *settableDoc) JSONSet(token string, data any) error { switch token { case "a": switch dt := data.(type) { case settableColl: s.Coll = dt return nil case *settableColl: if dt != nil { s.Coll = *dt } else { s.Coll = settableColl{} } return nil case []settableCollItem: s.Coll.Items = dt return nil } case "d": switch dt := data.(type) { case settableInt: s.Int = dt return nil case int: s.Int.Value = dt return nil case int8: s.Int.Value = int(dt) return nil case int16: s.Int.Value = int(dt) return nil case int32: s.Int.Value = int(dt) return nil case int64: s.Int.Value = int(dt) return nil default: return fmt.Errorf("invalid type %T for %s: %w", data, token, ErrPointer) } } return fmt.Errorf("%s is not a known field: %w", token, ErrPointer) } type settableColl struct { Items []settableCollItem } func (s settableColl) MarshalJSON() ([]byte, error) { return json.Marshal(s.Items) } func (s *settableColl) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &s.Items) } // JSONLookup implements an interface to customize json pointer lookup. func (s settableColl) JSONLookup(token string) (any, error) { if tok, err := strconv.Atoi(token); err == nil { return &s.Items[tok], nil } return nil, fmt.Errorf("%s is not a valid index: %w", token, ErrPointer) } // JSONLookup implements an interface to customize json pointer lookup. func (s *settableColl) JSONSet(token string, data any) error { if _, err := strconv.Atoi(token); err == nil { _, err := SetForToken(s.Items, token, data) return err } return fmt.Errorf("%s is not a valid index: %w", token, ErrPointer) } type settableCollItem struct { B int `json:"b"` C int `json:"c"` } type settableInt struct { Value int } func (s settableInt) MarshalJSON() ([]byte, error) { return json.Marshal(s.Value) } func (s *settableInt) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &s.Value) } func TestSetNode(t *testing.T) { t.Parallel() const jsonText = `{"a":[{"b": 1, "c": 2}], "d": 3}` var jsonDocument any require.NoError(t, json.Unmarshal([]byte(jsonText), &jsonDocument)) t.Run("with set node c", func(t *testing.T) { const in = "/a/0/c" p, err := New(in) require.NoError(t, err) _, err = p.Set(jsonDocument, 999) require.NoError(t, err) firstNode, ok := jsonDocument.(map[string]any) require.TrueT(t, ok) assert.Len(t, firstNode, 2) sliceNode, ok := firstNode["a"].([]any) require.TrueT(t, ok) assert.Len(t, sliceNode, 1) changedNode, ok := sliceNode[0].(map[string]any) require.TrueT(t, ok) chNodeVI := changedNode["c"] require.IsType(t, 0, chNodeVI) changedNodeValue, ok := chNodeVI.(int) require.TrueT(t, ok) require.EqualT(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") 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.TrueT(t, ok) assert.Len(t, firstNode, 2) sliceNode, ok := firstNode["a"].([]any) require.TrueT(t, ok) assert.Len(t, sliceNode, 1) changedNode, ok := sliceNode[0].(map[string]any) require.TrueT(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) _, 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.EqualT(t, 4, changedNode.B) assert.EqualT(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) _, 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.EqualT(t, 3, changedNode.B) assert.EqualT(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) _, err = p.Set(&structDoc, 999) require.NoError(t, err) require.Len(t, structDoc.A, 1) assert.EqualT(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) _, 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.EqualT(t, 4, changedNode.B) assert.EqualT(t, 7, changedNode.C) }) t.Run("with node 0", func(t *testing.T) { v, err := New("/a/0") require.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.EqualT(t, 3, changedNode.B) assert.EqualT(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.EqualT(t, 999, setDoc.Coll.Items[0].C) }) }) t.Run("with nil traversal panic", func(t *testing.T) { // This test exposes the panic that occurs when trying to set a value // through a path that contains nil intermediate values data := map[string]any{ "level1": map[string]any{ "level2": map[string]any{ "level3": nil, // This nil causes the panic }, }, } ptr, err := New("/level1/level2/level3/value") require.NoError(t, err) // This should return an error, not panic _, err = ptr.Set(data, "test-value") // The library should handle this gracefully and return an error // instead of panicking require.Error(t, err, "Setting value through nil intermediate path should return an error, not panic") }) t.Run("with direct nil map value", func(t *testing.T) { // Simpler test case that directly tests nil traversal data := map[string]any{ "container": nil, } ptr, err := New("/container/nested/value") require.NoError(t, err) // Attempting to traverse through nil should return an error, not panic _, err = ptr.Set(data, "test") require.Error(t, err, "Cannot traverse through nil intermediate values") }) t.Run("with nil in nested structure", func(t *testing.T) { // Test case with multiple nil values in nested structure data := map[string]any{ "config": map[string]any{ "settings": nil, }, "data": map[string]any{ "nested": map[string]any{ "properties": map[string]any{ "attributes": nil, // Nil intermediate value }, }, }, } ptr, err := New("/data/nested/properties/attributes/name") require.NoError(t, err) // Should return error, not panic _, err = ptr.Set(data, "test-name") require.Error(t, err, "Setting through nil intermediate path should return error") }) t.Run("with path creation through nil intermediate", func(t *testing.T) { // Test case that simulates path creation functions encountering nil // This happens when tools try to create missing paths but encounter nil intermediate values data := map[string]any{ "spec": map[string]any{ "template": nil, // This blocks path creation attempts }, } // Attempting to create a path like /spec/template/metadata/labels should fail gracefully ptr, err := New("/spec/template/metadata") require.NoError(t, err) // Should return error when trying to set on nil intermediate during path creation _, err = ptr.Set(data, map[string]any{"labels": map[string]any{}}) require.Error(t, err, "Setting on nil intermediate during path creation should return error") }) t.Run("with SetForToken on nil", func(t *testing.T) { // Test the single-level SetForToken function with nil data := map[string]any{ "container": nil, } // Should handle nil gracefully at single token level _, err := SetForToken(data["container"], "nested", "value") require.Error(t, err, "SetForToken on nil should return error, not panic") }) } func TestOffset(t *testing.T) { t.Parallel() 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: 4, }, { 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, }, { name: "array element after nested object", ptr: "/1", input: `[{"x":1}, 42]`, offset: 10, }, { name: "array element after nested array", ptr: "/1", input: `[[1,2], 42]`, offset: 8, }, { name: "array element after mixed composites", ptr: "/2", input: `[{"x":1}, [3,4], 42]`, offset: 17, }, { name: "object key after scalar sibling", ptr: "/b", input: `{"a": 1, "b": 2}`, offset: 9, }, { name: "object key after composite sibling", ptr: "/bar", input: `{"foo": {}, "bar": 1}`, offset: 12, }, { name: "whitespace between value and comma", ptr: "/b", input: `{"a":1 ,"b":2}`, offset: 8, }, { name: "array index is not a number", ptr: "/foo", input: `[1,2,3]`, hasError: true, }, { name: "pointer traverses through a scalar", ptr: "/foo/bar", input: `{"foo": 42}`, hasError: true, }, { name: "malformed JSON document", ptr: "/0", input: `not json`, hasError: true, }, { name: "missing object key with nested composite siblings", ptr: "/c", input: `{"a":{}, "b":[]}`, hasError: true, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ptr, err := New(tt.ptr) require.NoError(t, err) offset, err := ptr.Offset(tt.input) if tt.hasError { require.Error(t, err) return } t.Log(offset, err) require.NoError(t, err) assert.EqualT(t, tt.offset, offset) }) } } func TestEdgeCases(t *testing.T) { t.Parallel() t.Run("set at pointer against an unsupported type (int) should error", func(t *testing.T) { p, err := New("/invalid") require.NoError(t, err) _, err = p.Set(1, 1234) require.Error(t, err) require.ErrorIs(t, err, ErrUnsupportedValueType) }) t.Run("set with empty pointer", func(t *testing.T) { p, err := New("") require.NoError(t, err) doc := testDocumentJSON(t) newDoc, err := p.Set(doc, 1) require.NoError(t, err) require.Equal(t, doc, newDoc) }) t.Run("with out of bounds index", func(t *testing.T) { p, err := New("/foo/10") require.NoError(t, err) t.Run("should error on Get", func(t *testing.T) { _, _, err := p.Get(testStructJSONDoc(t)) require.Error(t, err) require.ErrorContains(t, err, "index out of bounds") }) t.Run("should error on Set", func(t *testing.T) { _, err := p.Set(testStructJSONPtr(t), "peek-a-boo") require.Error(t, err) require.ErrorContains(t, err, "index out of bounds") }) }) t.Run("Set with invalid pointer token", func(t *testing.T) { doc := testStructJSONDoc(t) pointer, err := New("/foo/x") require.NoError(t, err) _, err = pointer.Set(&doc, "yay") require.Error(t, err) require.ErrorContains(t, err, `Atoi: parsing "x"`) }) t.Run("Set with invalid reference in struct", func(t *testing.T) { doc := struct { A func() `json:"a"` B []int `json:"b"` }{ A: func() {}, B: []int{0, 1}, } t.Run("should error when attempting to set a struct field value that is not assignable", func(t *testing.T) { pointerA, err := New("/a") require.NoError(t, err) _, err = pointerA.Set(&doc, "waou") require.Error(t, err) require.ErrorContains(t, err, `can't set value with type string to field A`) }) t.Run("should error when attempting to set a slice element value that is not assignable", func(t *testing.T) { pointerB, err := New("/b/0") require.NoError(t, err) _, err = pointerB.Set(&doc, "waou") require.Error(t, err) require.ErrorContains(t, err, `can't set value with type string to slice element 0 with type int`) }) t.Run("should error when attempting to set a value that does not exist", func(t *testing.T) { pointerB, err := New("/x") require.NoError(t, err) _, _, err = pointerB.Get(&doc) require.Error(t, err) require.ErrorContains(t, err, `no field`) _, err = pointerB.Set(&doc, "oops") require.Error(t, err) require.ErrorContains(t, err, `no field`) }) }) } func TestInternalEdgeCases(t *testing.T) { t.Parallel() t.Run("setSingleImpl should error on any node not a struct, map or slice", func(t *testing.T) { var node int _, err := setSingleImpl(&node, 3, "a", jsonname.DefaultJSONNameProvider) require.Error(t, err) require.ErrorContains(t, err, `invalid token reference "a"`) }) t.Run("with simulated unsettable", func(t *testing.T) { type unsettable struct { A string `json:"a"` } doc := unsettable{ A: "a", } t.Run("setSingleImpl should error on struct field that is not settable", func(t *testing.T) { node := doc // doesn't pass a pointer: unsettable _, err := setSingleImpl(node, "new value", "a", jsonname.DefaultJSONNameProvider) require.Error(t, err) require.ErrorContains(t, err, `can't set struct field`) }) }) t.Run("assignReflectValue is a no-op when src is an untyped nil", func(t *testing.T) { target := "unchanged" dst := reflect.ValueOf(&target).Elem() assignReflectValue(dst, nil) require.Equal(t, "unchanged", target) }) } func TestSetIntermediateErrors(t *testing.T) { t.Parallel() type leaf struct { V string `json:"v"` } type doc struct { M map[string]leaf `json:"m"` L []leaf `json:"l"` S leaf `json:"s"` N int `json:"n"` P pointableImpl `json:"p"` } newDoc := func() *doc { return &doc{ M: map[string]leaf{"present": {V: "x"}}, L: []leaf{{V: "a"}, {V: "b"}}, P: pointableImpl{a: "hello"}, } } cases := []struct { name string pointer string substr string }{ { name: "map missing key mid-path", pointer: "/m/missing/v", substr: `no key "missing"`, }, { name: "slice non-numeric index mid-path", pointer: "/l/abc/v", substr: `parsing "abc"`, }, { name: "slice out-of-bounds mid-path", pointer: "/l/99/v", substr: `out of bounds`, }, { name: "struct unknown field mid-path", pointer: "/s/bogus/v", substr: `no field "bogus"`, }, { name: "scalar traversal mid-path", pointer: "/n/anything/v", substr: `invalid token reference "anything"`, }, { name: "JSONPointable returns error mid-path", pointer: "/p/unknown/v", substr: `no field "unknown"`, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ptr, err := New(tt.pointer) require.NoError(t, err) _, err = ptr.Set(newDoc(), "value") require.Error(t, err) require.ErrorIs(t, err, ErrPointer) require.ErrorContains(t, err, tt.substr) }) } } go-openapi-jsonpointer-d29978d/struct_example_test.go000066400000000000000000000046441517066760400231450ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer_test import ( "errors" "fmt" "github.com/go-openapi/jsonpointer" ) var ErrExampleIface = errors.New("example error") type ExampleDoc struct { PromotedDoc Promoted EmbeddedDoc `json:"promoted"` AnonPromoted EmbeddedDoc `json:"-"` A string `json:"propA"` Ignored string `json:"-"` Untagged string unexported string } type EmbeddedDoc struct { B string `json:"propB"` } type PromotedDoc struct { C string `json:"propC"` } func Example_struct() { doc := ExampleDoc{ PromotedDoc: PromotedDoc{ C: "c", }, Promoted: EmbeddedDoc{ B: "promoted", }, A: "a", Ignored: "ignored", unexported: "unexported", } { // tagged simple field pointerA, _ := jsonpointer.New("/propA") a, _, err := pointerA.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf("a: %v\n", a) } { // tagged struct field is resolved pointerB, _ := jsonpointer.New("/promoted/propB") b, _, err := pointerB.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf("b: %v\n", b) } { // tagged embedded field is resolved pointerC, _ := jsonpointer.New("/propC") c, _, err := pointerC.Get(doc) if err != nil { fmt.Println(err) return } fmt.Printf("c: %v\n", c) } { // exlicitly ignored by JSON tag. pointerI, _ := jsonpointer.New("/ignored") _, _, err := pointerI.Get(doc) fmt.Printf("ignored: %v\n", err) } { // unexported field is ignored: use [JSONPointable] to alter this behavior. pointerX, _ := jsonpointer.New("/unexported") _, _, err := pointerX.Get(doc) fmt.Printf("unexported: %v\n", err) } { // Limitation: anonymous field is not resolved. pointerC, _ := jsonpointer.New("/propB") _, _, err := pointerC.Get(doc) fmt.Printf("anonymous: %v\n", err) } { // Limitation: untagged exported field is ignored, unlike with json standard MarshalJSON. pointerU, _ := jsonpointer.New("/untagged") _, _, err := pointerU.Get(doc) fmt.Printf("untagged: %v\n", err) } // output: // a: a // b: promoted // c: c // ignored: object has no field "ignored": JSON pointer error // unexported: object has no field "unexported": JSON pointer error // anonymous: object has no field "propB": JSON pointer error // untagged: object has no field "untagged": JSON pointer error } go-openapi-jsonpointer-d29978d/testdata/000077500000000000000000000000001517066760400203215ustar00rootroot00000000000000go-openapi-jsonpointer-d29978d/testdata/test_document.json000066400000000000000000000005131517066760400240700ustar00rootroot00000000000000{ "foo": [ "bar", "baz" ], "obj": { "a":1, "b":2, "c":[ 3, 4 ], "d":[ { "e":9 }, { "f":[ 50, 51 ] } ] }, "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 } go-openapi-jsonpointer-d29978d/testdata_test.go000066400000000000000000000023121517066760400217050ustar00rootroot00000000000000// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 package jsonpointer import ( _ "embed" // initialize embed "encoding/json" "testing" "github.com/go-openapi/testify/v2/require" ) //go:embed testdata/*.json var testDocumentJSONBytes []byte func testDocumentJSON(t *testing.T) any { t.Helper() var document any require.NoError(t, json.Unmarshal(testDocumentJSONBytes, &document)) return document } func testStructJSONDoc(t *testing.T) testStructJSON { t.Helper() var document testStructJSON require.NoError(t, json.Unmarshal(testDocumentJSONBytes, &document)) return document } func testStructJSONPtr(t *testing.T) *testStructJSON { t.Helper() document := testStructJSONDoc(t) return &document } // number of items in the test document. func testDocumentNBItems() int { return 11 } // number of objects nodes in the test document. func testNodeObjNBItems() int { return 4 } type testStructJSON struct { Foo []string `json:"foo"` Obj struct { A int `json:"a"` B int `json:"b"` C []int `json:"c"` D []struct { E int `json:"e"` F []int `json:"f"` } `json:"d"` } `json:"obj"` } type aliasedMap map[string]any