vendor: update buildkit to v0.31.0-rc2

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-06-12 13:46:40 +02:00
parent d1c298cd85
commit 268dcf5917
351 changed files with 11926 additions and 4883 deletions
-181
View File
@@ -1,181 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
header = """
"""
footer = """
-----
**[{{ remote.github.repo }}]({{ self::remote_url() }}) license terms**
[![License][license-badge]][license-url]
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
[license-url]: {{ self::remote_url() }}/?tab=Apache-2.0-1-ov-file#readme
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
body = """
{%- if version %}
## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{%- else %}
## [unreleased]
{%- endif %}
{%- if message %}
{%- raw %}\n{% endraw %}
{{ message }}
{%- raw %}\n{% endraw %}
{%- endif %}
{%- if version %}
{%- if previous.version %}
**Full Changelog**: <{{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}>
{%- endif %}
{%- else %}
{%- raw %}\n{% endraw %}
{%- endif %}
{%- if statistics %}{% if statistics.commit_count %}
{%- raw %}\n{% endraw %}
{{ statistics.commit_count }} commits in this release.
{%- raw %}\n{% endraw %}
{%- endif %}{% endif %}
-----
{%- for group, commits in commits | group_by(attribute="group") %}
{%- raw %}\n{% endraw %}
### {{ group | upper_first }}
{%- raw %}\n{% endraw %}
{%- for commit in commits %}
{%- if commit.remote.pr_title %}
{%- set commit_message = commit.remote.pr_title %}
{%- else %}
{%- set commit_message = commit.message %}
{%- endif %}
* {{ commit_message | split(pat="\n") | first | trim }}
{%- if commit.remote.username %}
{%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }})
{%- endif %}
{%- if commit.remote.pr_number %}
{%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})
{%- endif %}
{%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }})
{%- endfor %}
{%- endfor %}
{%- if github %}
{%- raw %}\n{% endraw -%}
{%- set all_contributors = github.contributors | length %}
{%- if github.contributors | filter(attribute="username", value="dependabot[bot]") | length < all_contributors %}
-----
### People who contributed to this release
{% endif %}
{%- for contributor in github.contributors | filter(attribute="username") | sort(attribute="username") %}
{%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
* [@{{ contributor.username }}](https://github.com/{{ contributor.username }})
{%- endif %}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
-----
{%- raw %}\n{% endraw %}
### New Contributors
{%- endif %}
{%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
{%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %}
in [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- raw %}\n{% endraw %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = false
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = false
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' }
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^[Cc]hore\\([Rr]elease\\): prepare for", skip = true },
{ message = "(^[Mm]erge)|([Mm]erge conflict)", skip = true },
{ field = "author.name", pattern = "dependabot*", group = "<!-- 0A -->Updates" },
{ message = "([Ss]ecurity)|([Vv]uln)", group = "<!-- 08 -->Security" },
{ body = "(.*[Ss]ecurity)|([Vv]uln)", group = "<!-- 08 -->Security" },
{ message = "([Cc]hore\\(lint\\))|(style)|(lint)|(codeql)|(golangci)", group = "<!-- 05 -->Code quality" },
{ message = "(^[Dd]oc)|((?i)readme)|(badge)|(typo)|(documentation)", group = "<!-- 03 -->Documentation" },
{ message = "(^[Ff]eat)|(^[Ee]nhancement)", group = "<!-- 00 -->Implemented enhancements" },
{ message = "(^ci)|(\\(ci\\))|(fixup\\s+ci)|(fix\\s+ci)|(license)|(example)", group = "<!-- 07 -->Miscellaneous tasks" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "(^fix)|(panic)", group = "<!-- 01 -->Fixed bugs" },
{ message = "(^refact)|(rework)", group = "<!-- 02 -->Refactor" },
{ message = "(^[Pp]erf)|(performance)", group = "<!-- 04 -->Performance" },
{ message = "(^[Cc]hore)", group = "<!-- 07 -->Miscellaneous tasks" },
{ message = "^[Rr]evert", group = "<!-- 09 -->Reverted changes" },
{ message = "(upgrade.*?go)|(go\\s+version)", group = "<!-- 0A -->Updates" },
{ message = ".*", group = "<!-- 0B -->Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order releases topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "newest"
# Process submodules commits
recurse_submodules = false
#[remote.github]
#owner = "go-openapi"
-1
View File
@@ -3,4 +3,3 @@
.idea
.env
.mcp.json
.claude/
+4 -3
View File
@@ -4,11 +4,11 @@
| Total Contributors | Total Contributions |
| --- | --- |
| 12 | 101 |
| 13 | 111 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @fredbi | 54 | <https://github.com/go-openapi/jsonpointer/commits?author=fredbi> |
| @fredbi | 63 | <https://github.com/go-openapi/jsonpointer/commits?author=fredbi> |
| @casualjim | 33 | <https://github.com/go-openapi/jsonpointer/commits?author=casualjim> |
| @magodo | 3 | <https://github.com/go-openapi/jsonpointer/commits?author=magodo> |
| @youyuanwu | 3 | <https://github.com/go-openapi/jsonpointer/commits?author=youyuanwu> |
@@ -18,7 +18,8 @@
| @ianlancetaylor | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=ianlancetaylor> |
| @mfleader | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=mfleader> |
| @Neo2308 | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=Neo2308> |
| @alexandear | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=alexandear> |
| @olivierlemasle | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=olivierlemasle> |
| @testwill | 1 | <https://github.com/go-openapi/jsonpointer/commits?author=testwill> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
+1 -1
View File
@@ -18,7 +18,7 @@ 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/sigh-399/jsonpointer
github.com/sigu-399/jsonpointer
===========================
// SPDX-FileCopyrightText: Copyright 2013 sigu-399 ( https://github.com/sigu-399 )
+31 -21
View File
@@ -16,17 +16,25 @@ An implementation of JSON Pointer for golang, which supports go `struct`.
## Announcements
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
* **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 "-"
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
* **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.
API is stable and feature-complete.
## Import this library in your project
@@ -88,7 +96,7 @@ See <https://github.com/go-openapi/jsonpointer/releases>
<https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07>
also known as [RFC6901](https://www.rfc-editor.org/rfc/rfc6901)
also known as [RFC6901][RFC6901].
## Licensing
@@ -99,19 +107,19 @@ on top of which it has been built.
## Limitations
The 4.Evaluation part of the previous reference, starting with 'If the currently referenced value is a JSON array,
the reference token MUST contain either...' is not implemented.
That is because our implementation of the JSON pointer only supports explicit references to array elements:
the provision in the spec to resolve non-existent members as "the last element in the array",
using the special trailing character "-" is not implemented.
* [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](.github/CONTRIBUTING.md)
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
* [Contributing guidelines][contributing-doc-site]
* [Maintainers documentation][maintainers-doc-site]
* [Code style][style-doc-site]
## Cutting a new release
@@ -142,11 +150,8 @@ Maintainers can cut a new release by either:
<!-- Badges: documentation & support -->
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/jsonpointer
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/jsonpointer
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
[discord-url]: https://discord.gg/FfnFYaC3k5
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
@@ -156,3 +161,8 @@ Maintainers can cut a new release by either:
[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
<!-- Organization docs -->
[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
+25 -1
View File
@@ -16,12 +16,24 @@ const (
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
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)
}
@@ -33,3 +45,15 @@ func errOutOfBounds(length, idx int) error {
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)
}
+47
View File
@@ -0,0 +1,47 @@
// 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)
}
+86
View File
@@ -0,0 +1,86 @@
// 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
}
+273 -85
View File
@@ -11,8 +11,6 @@ import (
"reflect"
"strconv"
"strings"
"github.com/go-openapi/swag/jsonname"
)
const (
@@ -20,20 +18,6 @@ const (
pointerSeparator = `/`
)
// 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.
type JSONSetable interface {
// JSONSet sets the value pointed at the (unescaped) key.
JSONSet(key string, value any) error
}
// Pointer is a representation of a json pointer.
//
// Use [Pointer.Get] to retrieve a value or [Pointer.Set] to set a value.
@@ -41,7 +25,7 @@ type JSONSetable interface {
// 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 [JSONPointable.JSONSet] method is used to resolve [Pointer.Set]
// - 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
@@ -71,16 +55,35 @@ func New(jsonPointerString string) (Pointer, error) {
// 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) (any, reflect.Kind, error) {
return p.get(document, jsonname.DefaultJSONNameProvider)
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.
//
// It returns the updated document.
func (p *Pointer) Set(document any, value any) (any, error) {
return document, p.set(document, value, jsonname.DefaultJSONNameProvider)
// # 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.
@@ -109,6 +112,46 @@ func (p *Pointer) String() string {
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
@@ -137,7 +180,35 @@ func (p *Pointer) Offset(document string) (int64, error) {
return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer)
}
}
return offset, nil
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.
@@ -157,9 +228,9 @@ func (p *Pointer) parse(jsonPointerString string) error {
return nil
}
func (p *Pointer) get(node any, nameProvider *jsonname.NameProvider) (any, reflect.Kind, error) {
func (p *Pointer) get(node any, nameProvider NameProvider) (any, reflect.Kind, error) {
if nameProvider == nil {
nameProvider = jsonname.DefaultJSONNameProvider
nameProvider = defaultOptions.provider
}
kind := reflect.Invalid
@@ -185,50 +256,130 @@ func (p *Pointer) get(node any, nameProvider *jsonname.NameProvider) (any, refle
return node, kind, nil
}
func (p *Pointer) set(node, data any, nameProvider *jsonname.NameProvider) error {
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 errors.Join(
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,
)
}
l := len(p.referenceTokens)
// full document when empty
if l == 0 {
return nil
if len(p.referenceTokens) == 0 {
return node, nil
}
if nameProvider == nil {
nameProvider = jsonname.DefaultJSONNameProvider
nameProvider = defaultOptions.provider
}
var decodedToken string
lastIndex := l - 1
if lastIndex > 0 { // skip if we only have one token in pointer
for _, token := range p.referenceTokens[:lastIndex] {
decodedToken = Unescape(token)
next, err := p.resolveNodeForToken(node, decodedToken, nameProvider)
if err != nil {
return err
}
node = next
}
}
// last token
decodedToken = Unescape(p.referenceTokens[lastIndex])
return setSingleImpl(node, data, decodedToken, nameProvider)
return p.setAt(node, p.referenceTokens, data, nameProvider)
}
func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider *jsonname.NameProvider) (next any, err error) {
// 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)
@@ -272,6 +423,9 @@ func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvide
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)
@@ -312,16 +466,23 @@ func typeFromValue(v reflect.Value) any {
}
// GetForToken gets a value for a json pointer token 1 level deep.
func GetForToken(document any, decodedToken string) (any, reflect.Kind, error) {
return getSingleImpl(document, decodedToken, jsonname.DefaultJSONNameProvider)
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.
func SetForToken(document any, decodedToken string, value any) (any, error) {
return document, setSingleImpl(document, value, decodedToken, jsonname.DefaultJSONNameProvider)
//
// 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 *jsonname.NameProvider) (any, reflect.Kind, error) {
func getSingleImpl(node any, decodedToken string, nameProvider NameProvider) (any, reflect.Kind, error) {
rValue := reflect.Indirect(reflect.ValueOf(node))
kind := rValue.Kind()
if isNil(node) {
@@ -361,6 +522,9 @@ func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NamePro
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)
@@ -378,14 +542,14 @@ func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NamePro
}
}
func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.NameProvider) error {
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 fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer)
return node, fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer)
}
if ns, ok := node.(JSONSetable); ok {
return ns.JSONSet(decodedToken, data)
return node, ns.JSONSet(decodedToken, data)
}
rValue := reflect.Indirect(reflect.ValueOf(node))
@@ -394,12 +558,12 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
case reflect.Struct:
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
if !ok {
return fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
return node, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer)
}
fld := rValue.FieldByName(nm)
if !fld.CanSet() {
return fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer)
return node, fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer)
}
value := reflect.ValueOf(data)
@@ -407,33 +571,51 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
assignedType := fld.Type()
if !valueType.AssignableTo(assignedType) {
return fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer)
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 nil
return node, nil
case reflect.Map:
kv := reflect.ValueOf(decodedToken)
rValue.SetMapIndex(kv, reflect.ValueOf(data))
return nil
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 errors.Join(err, ErrPointer)
return node, errors.Join(err, ErrPointer)
}
sLength := rValue.Len()
if tokenIndex < 0 || tokenIndex >= sLength {
return errOutOfBounds(sLength, tokenIndex)
return node, errOutOfBounds(sLength, tokenIndex)
}
elem := rValue.Index(tokenIndex)
if !elem.CanSet() {
return fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer)
return node, fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer)
}
value := reflect.ValueOf(data)
@@ -441,15 +623,15 @@ func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.N
assignedType := elem.Type()
if !valueType.AssignableTo(assignedType) {
return fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer)
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 nil
return node, nil
default:
return errInvalidReference(decodedToken)
return node, errInvalidReference(decodedToken)
}
}
@@ -460,24 +642,27 @@ func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) {
if err != nil {
return 0, err
}
switch tk := tk.(type) {
case json.Delim:
switch tk {
case '{':
if err = drainSingle(dec); err != nil {
return 0, err
}
case '[':
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
}
}
case string:
if tk == decodedToken {
return offset, nil
}
default:
return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer)
}
}
@@ -485,6 +670,9 @@ func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) {
}
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)