Issue 114#1197
Merged
Merged
Conversation
The `#/key` fragment previously returned only string leaf values; numbers, booleans and nested structures came back as nil or an error. isTerminalValue now treats every scalar kind (floats, large ints, etc.) as a terminal value, and the fragment traversal returns the value at the final key whatever its type, so a number or a nested object/array is extractable as well. This lives in the shared traversal in vals.go, so it fixes every provider that supports fragments (s3, file, gcs, ...), not just s3. Verified without AWS via the file provider: TestFragment_Issue114_FileProvider, TestFragment_ScalarTypes, TestFragment_NestedObjectAndArray, and an extended TestEvalNodesTypes covering the full CLI path. Signed-off-by: Alexander Kulikov <99725186+askripe@users.noreply.github.com>
Not required for helmfile#114, but two adjacent correctness bugs found while fixing it: - asStringKeyedMap: descending through an intermediate map[interface{}]interface{} (which some providers produce) previously shadowed the outer variable and silently failed to descend. - comma-ok lookup distinguishes a genuinely absent key (at any depth) from a present null value, so FailOnMissingKeyInMap behaves correctly. Covered by TestFragment_IntermediateInterfaceMap and TestFragment_MissingKeyAndNil (incl. a missing nested key). Signed-off-by: Alexander Kulikov <99725186+askripe@users.noreply.github.com>
yxxhero
approved these changes
Jun 21, 2026
yxxhero
left a comment
Member
There was a problem hiding this comment.
Review: PR #1197 "Issue 114"
Verdict: Approve. Solid fix for a long-standing bug (#114, open since 2021), with good test coverage and a clear writeup.
What it does
Rewrites the shared #/key fragment traversal in vals.go:481 so a fragment can extract any type — floats, wide ints, nested maps, arrays — not just string/int/bool. Lives in the shared path, so s3, gcs, file, vault, etc. all benefit.
Strengths
isTerminalValue(vals.go:516) now covers all scalar kinds, matching the set already used inpkg/expansion/expand_match.go. Consistent.- Genuine bug fixes found along the way (commit 2):
vals.go:494— the oldcase map[interface{}]interface{}declaredobj :=inside the case, shadowing the outer variable, so it never actually descended.asStringKeyedMapreturns the normalized map and assigns to the outerobj. Real fix.t, found := obj[k]correctly distinguishes an absent key from a present YAMLnull. Previously a missing key withFailOnMissingKeyInMap=truehit thedefaultbranch and produced the confusingunsupported type ... <nil>error; theFailOnMissingKeyInMapcheck at the end of the loop was effectively unreachable. Now it fires correctly at any depth.
- Tests (
vals_fragment_test.go) are table-driven, use the file provider (no AWS creds), and cover: int/float/object extraction,uint64 > MaxInt64, negative floats, nested object/array, intermediatemap[interface{}]interface{}via mock provider, and missing-key-vs-null semantics.TestEvalNodesTypesextends the full CLI path.go test -race ./...is green locally. - README change accurately reflects the new behavior.
Minor observations (non-blocking)
vals.go:508return nil, nilafter the loop is now unreachable — the loop always returns on the last index, on!found, or on a non-map intermediate. Harmless (kept the compiler happy), but dead code.- No test for a final value that is itself
map[interface{}]interface{}.TestFragment_IntermediateInterfaceMaponly descends through one to a string leaf. If a provider returns a nestedmap[interface{}]interface{}and the fragment points directly at it, it's returned as-is (not normalized tomap[string]interface{}). This is consistent with the PR's "native type preserved" goal andyaml.Marshalhandles it, but a test asserting the returned shape would lock in the contract. - Nested map/slice results aren't cached under
key(theisTerminalValueguard beforer.docCache.Addskips them). This matches the pre-PR behavior and the document is still cached undermapRequestURI, so the only cost is re-traversal — negligible. Noting for completeness.
Behavior change worth signaling to users
A missing key now returns nil (or errors with no value found for key ... under FailOnMissingKeyInMap) instead of the old unsupported type for key ... <nil>. The PR description calls this out. Existing string/int/bool extraction is unchanged.
Recommend merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A
#/keyfragment could only pull string leaf values out of a YAML/JSONsecret. After #934
intandboolwork too, but floating-point numbers, otherinteger widths, and keys that point at a nested object or array still fail. This
makes all of them work, preserving their native types.
Fixes #114.
Why
The fragment traversal in
vals.godecided what to return based on whether thevalue was a "terminal" scalar, and
isTerminalValueonly countedbool,intand
string. As a result:float64(the usual JSON number) or a large integer fell through tounsupported type for key;nil/no value found for key), so you could not pull a sub-document out of akey.
Changes
All in the shared traversal, so every fragment-capable provider benefits
(s3, gcs, file, vault, ...), not just s3:
isTerminalValuenow accepts every scalar kind (float32/64, all sizedint/uint), reusing the set already defined for nested refs inpkg/expansion/expand_match.go.(scalar, nested map, or slice).
map[interface{}]interface{}(which someproviders produce) no longer silently fails (a shadowed-variable bug).
nullvia a comma-oklookup, so
FailOnMissingKeyInMapis honored and a presentnullreturnsnil.Example
s3://bucket/obj.json={"key": 123, "ratio": 1.5, "obj": {"a": 1}, "list": ["x", "y"]}...#/key(int)123123...#/ratio(float)unsupported type for key (float64)1.5...#/obj(object)nil/no value found for key{a: 1}...#/list(array)unsupported type for key[x, y]...#/strkey(string)Behavior change to note
A missing key previously surfaced as a confusing
unsupported type for key ... <nil>. It now returnsnil, or, withFailOnMissingKeyInMap, the clearerno value found for key .... Existing string/int/bool extraction is unchanged.Testing
New table-driven tests use the file provider (it shares s3's
GetStringMap/yaml.Unmarshal), so they need no cloud credentials, plus amock-provider test for the
map[interface{}]interface{}path, and an extendedTestEvalNodesTypescovering the full CLI path.go test -race ./...is green