Documentation
¶
Overview ¶
Package jsonkeys implements per-key JSON pointer merge used by adapters that need to own a subset of keys inside a shared JSON (or JSONC) config file.
It is also the single home for RFC 6901 pointer mechanics (pointer.go): EscapeToken and UnescapeToken for reference tokens, SplitPointer, and Get, the one pointer resolver every package walks a document with.
Index ¶
- func ConvertNumbers(v any) any
- func DecodeJSONC(data []byte) (map[string]any, error)
- func DecodeObject(data []byte) (map[string]any, error)
- func DecodeYAML(yml []byte) (map[string]any, error)
- func EscapeToken(s string) string
- func Get(m map[string]any, ptr string) (any, bool)
- func MergeJSONC(existing []byte, ours map[string]any, ownedPointers []string) ([]byte, error)
- func MergeKeys(existing, ours map[string]any, ownedPointers []string) (map[string]any, []string, []string)
- func SplitPointer(ptr string) []string
- func UnescapeToken(tok string) string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ConvertNumbers ¶ added in v0.11.0
ConvertNumbers walks v, replacing every json.Number with a Go numeric, recursing through maps and slices and leaving non-numeric values untouched. DecodeObject/DecodeJSONC deliberately KEEP json.Number so a merge re-marshals foreign JSON keys with their exact digits — but a json.Number that escapes into a TOML-persisted field is a hazard: go-toml marshals the underlying string type as a TOML *string* (`timeout = '30'`), silently flipping the value's type on the next render. Callers that persist decoded values outside JSON (the capture dest->source funnel, DecodeYAML) normalize through this first.
Range contract: an integer within int64 stays exact (snowflake ids and nanosecond timestamps beyond float64's 2^53 survive); an integer BEYOND int64 falls to float64 and is lossy — TOML integers are int64, so exactness past that bound is unrepresentable in the persisted form anyway; a numeric literal that parses as neither int64 nor float64 (e.g. `1e309`, `1e400`) falls back to its literal string, persisted as a TOML string — acceptable for an unrepresentable extreme, and the capture leak backstop still scans strings.
func DecodeJSONC ¶ added in v0.2.0
DecodeJSONC parses JSONC content (comments + trailing commas tolerated) into a map, preserving json.Number. Plain JSON parses identically. Used by ingest paths whose native file is JSONC.
func DecodeObject ¶
DecodeObject unmarshals JSON object bytes into a map, preserving numbers as json.Number rather than coercing to float64. The merge promises to preserve FOREIGN keys verbatim, but float64 silently rounds any integer larger than 2^53 (snowflake ids, nanosecond timestamps) on re-marshal. json.Number keeps the literal digits and re-marshals exactly. nil/empty input yields an empty map.
Lax at the edges by design, and slightly laxer than json.Unmarshal: content AFTER the first top-level value is ignored rather than refused (json.Decoder reads one value), so a truncated-concatenation like `{…}{…}` decodes its first object instead of erroring; a corrupt single value still errors. Every JSON config read — adapter ingests AND the key-merge machinery — shares these semantics, so tightening here would change how merges read existing native files; callers that need whole-input strictness must enforce it themselves. Pinned by TestDecodeObject_LaxEdges.
func DecodeYAML ¶
DecodeYAML parses YAML (e.g. component frontmatter) into a map, preserving integer precision beyond 2^53 (up to int64). A plain yaml.Unmarshal into interface{} decodes every number as float64, silently rounding a large integer (snowflake id, nanosecond timestamp) — and since yaml.Marshal then re-emits the rounded value, the corruption is persisted on render and on source write-back. This routes YAML→JSON (which keeps the integer literal), decodes with UseNumber, then normalizes through ConvertNumbers so yaml.Marshal re-emits a bare integer; see ConvertNumbers' range contract for the beyond-int64 and unparseable extremes. Empty/null input yields an empty map; a non-mapping document is an error (frontmatter must be a mapping).
func EscapeToken ¶ added in v0.15.0
EscapeToken encodes one string as an RFC 6901 §3 reference token: '~' becomes "~0" and '/' becomes "~1".
ORDER IS LOAD-BEARING: '~' must be escaped FIRST. Escaping '/' first would turn "a/b" into "a~1b" and the subsequent '~' pass would re-escape that introduced tilde into "a~01b", which decodes back to "a~1b" — a silently corrupted key.
func Get ¶ added in v0.15.0
Get resolves ptr against m, walking only through objects. The bool is false when any segment is absent or when an intermediate value is not an object — distinct from a present value that happens to be null, which returns (nil, true). The empty pointer names the whole document and returns (m, true).
This is the one pointer resolver. internal/render's RecordOpsState relies on the presence signal to avoid recording a pointer that never landed on disk; the CLI's drift diagnostics discard it and treat absent as nil.
`/` yields no tokens and names the WHOLE document, not the `""` key RFC 6901 assigns it — the reading internal/render's resolver already had. The two CLI resolvers this replaced answered `/` with the `""` key instead, so this is the one pointer the copies disagreed on. It is reachable from a hand-edited state key, or from render.CollectPointers over a rendered document with a top-level `""` key, which no adapter renders today.
func MergeJSONC ¶ added in v0.2.0
MergeJSONC merges ours into existing JSONC content, removing ownedPointers no longer present in ours. Foreign keys and values are preserved. It is the shared engine behind the "merge-jsonc-keys" strategy (OpenCode's opencode.json, Gemini's settings.json, and the generic breadth tier's commented settings files).
Strategy: parse existing JSONC (hujson, tolerating comments + trailing commas) -> Standardize to strict JSON -> MergeKeys -> emit via json.MarshalIndent. v1 trade-off: this Standardize+marshal path discards ALL comments and trailing-comma formatting (not just touched sections). The file is never corrupted and no data keys are lost; only JSONC formatting is. Comment-preserving mutation is deferred to v1.x.
Unparseable existing content is a returned error, never treated as empty — merging against an empty map would clobber every foreign key in the file.
func MergeKeys ¶
func MergeKeys(existing, ours map[string]any, ownedPointers []string) (map[string]any, []string, []string)
MergeKeys merges ours into existing, removing ownedPointers that are no longer in ours. Returns the merged map plus diagnostic lists.
kept: pointers from ownedPointers that are still present in ours. removed: pointers from ownedPointers that are absent from ours and were deleted from existing.
JSON pointer syntax: leading "/", "/" separated path segments. RFC 6901 escapes ("~0" for "~", "~1" for "/") are supported.
func SplitPointer ¶ added in v0.15.0
SplitPointer splits a JSON pointer into its decoded reference tokens. A leading "/" is optional; the empty pointer (the whole document) yields no tokens — and so does "/": the leading slash is stripped BEFORE the empty check, so "/" is the empty pointer here, not the one-token pointer naming the "" key that RFC 6901 makes it. This is where the "/" reading Get documents is implemented.
func UnescapeToken ¶ added in v0.15.0
UnescapeToken decodes one RFC 6901 §3 reference token: "~1" is '/' and "~0" is '~'.
ORDER IS LOAD-BEARING, and it is the MIRROR of EscapeToken's: "~1" must be decoded FIRST. Decoding "~0" first would turn the literal token "~01" into "~1" and the subsequent pass would decode that into "/" — where the token means the two characters "~1".
Types ¶
This section is empty.