Skip to content

Commit 00fbb3f

Browse files
grahamalamaclaude
andcommitted
fix: address PR review feedback for ValuesFile implementation
- Align GetValue and SetValue lookup order (literal key first, then nested) - Fail fast when array notation references missing keys - Support integer, float, and bool scalar value types - Handle comment-only YAML files gracefully - Infer indentation from existing file structure instead of hardcoding - Simplify code: use type switches, remove redundant conditions - Fix column positioning for correct YAML serialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 13d768d commit 00fbb3f

File tree

2 files changed

+270
-147
lines changed

2 files changed

+270
-147
lines changed

pkg/argocd/values_file.go

Lines changed: 130 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,18 @@ func (y *ValuesFile) SetValue(key string, value string) error {
5454
doc := y.file.Docs[0]
5555
root := doc.Body
5656

57+
// Handle comment-only YAML or empty documents by initializing an empty mapping
5758
if root == nil {
58-
return fmt.Errorf("empty document body")
59+
root = makeEmptyMapping(0)
60+
doc.Body = root
61+
} else if commentGroup, isCommentOnly := root.(*ast.CommentGroupNode); isCommentOnly {
62+
// Comment-only YAML - create empty mapping and preserve comments
63+
newRoot := makeEmptyMapping(0)
64+
if err := newRoot.SetComment(commentGroup); err != nil {
65+
return fmt.Errorf("failed to preserve comments: %w", err)
66+
}
67+
doc.Body = newRoot
68+
root = newRoot
5969
}
6070

6171
rootMapping, ok := root.(*ast.MappingNode)
@@ -91,18 +101,14 @@ func (y *ValuesFile) GetValue(key string) (string, error) {
91101
return "", fmt.Errorf("root is not a mapping node")
92102
}
93103

94-
// First try nested path
95-
keys := strings.Split(key, ".")
96-
if val, err := getNestedValue(rootMapping, keys); err == nil {
97-
return val, nil
98-
}
99-
100-
// Try as literal key
104+
// First, check if the full key exists as a literal key
101105
if node := findKeyInMapping(rootMapping, key); node != nil {
102106
return getScalarValue(node)
103107
}
104108

105-
return "", fmt.Errorf("key %q not found", key)
109+
// Try navigating as a nested path
110+
keys := strings.Split(key, ".")
111+
return getNestedValue(rootMapping, keys)
106112
}
107113

108114
// findKeyInMapping finds a key in a mapping node and returns its value node.
@@ -115,51 +121,80 @@ func findKeyInMapping(mapping *ast.MappingNode, key string) ast.Node {
115121
return nil
116122
}
117123

118-
// setNodeValue sets the value of a scalar node, preserving comments.
119-
func setNodeValue(node ast.Node, value string) error {
120-
// Unwrap anchor/alias nodes
121-
node = unwrapNode(node)
124+
// updateNodeToken updates the token value for a node and returns its comment.
125+
func updateNodeToken(node ast.Node, value string) (*ast.CommentGroupNode, error) {
126+
type tokenNode interface {
127+
GetComment() *ast.CommentGroupNode
128+
GetToken() *token.Token
129+
}
122130

123-
stringNode, ok := node.(*ast.StringNode)
131+
tn, ok := node.(tokenNode)
124132
if !ok {
125-
return fmt.Errorf("cannot set value on non-scalar node (type: %T)", node)
133+
return nil, fmt.Errorf("cannot set value on non-scalar node (type: %T)", node)
126134
}
127135

128-
comment := stringNode.GetComment()
129-
stringNode.Value = value
130-
if comment != nil {
131-
if err := stringNode.SetComment(comment); err != nil {
132-
return fmt.Errorf("failed to preserve comment: %w", err)
136+
// Validate value based on node type
137+
switch n := node.(type) {
138+
case *ast.IntegerNode:
139+
if _, err := strconv.ParseInt(value, 10, 64); err != nil {
140+
return nil, fmt.Errorf("cannot set integer value to %q: %w", value, err)
141+
}
142+
case *ast.FloatNode:
143+
if _, err := strconv.ParseFloat(value, 64); err != nil {
144+
return nil, fmt.Errorf("cannot set float value to %q: %w", value, err)
145+
}
146+
case *ast.BoolNode:
147+
if _, err := strconv.ParseBool(value); err != nil {
148+
return nil, fmt.Errorf("cannot set bool value to %q: %w", value, err)
133149
}
150+
case *ast.StringNode:
151+
// String node - update the value field
152+
n.Value = value
134153
}
135154

136-
return nil
155+
// Update token for serialization
156+
if tok := tn.GetToken(); tok != nil {
157+
tok.Value = value
158+
}
159+
160+
return tn.GetComment(), nil
137161
}
138162

139-
// getScalarValue extracts the string value from a node.
140-
func getScalarValue(node ast.Node) (string, error) {
141-
// Handle alias nodes
142-
if aliasNode, ok := node.(*ast.AliasNode); ok {
143-
return getScalarValue(aliasNode.Value)
144-
}
163+
// setNodeValue sets the value of a scalar node, preserving comments and type.
164+
func setNodeValue(node ast.Node, value string) error {
165+
node = unwrapNode(node)
145166

146-
if stringNode, ok := node.(*ast.StringNode); ok {
147-
return stringNode.Value, nil
167+
comment, err := updateNodeToken(node, value)
168+
if err != nil {
169+
return err
148170
}
149171

150-
if intNode, ok := node.(*ast.IntegerNode); ok {
151-
return intNode.GetToken().Value, nil
172+
// Restore comment
173+
if comment != nil {
174+
if err := node.SetComment(comment); err != nil {
175+
return fmt.Errorf("failed to preserve comment: %w", err)
176+
}
152177
}
153178

154-
if floatNode, ok := node.(*ast.FloatNode); ok {
155-
return floatNode.GetToken().Value, nil
156-
}
179+
return nil
180+
}
157181

158-
if boolNode, ok := node.(*ast.BoolNode); ok {
159-
return boolNode.GetToken().Value, nil
182+
// getScalarValue extracts the string value from a node.
183+
func getScalarValue(node ast.Node) (string, error) {
184+
switch n := node.(type) {
185+
case *ast.AliasNode:
186+
return getScalarValue(n.Value)
187+
case *ast.StringNode:
188+
return n.Value, nil
189+
case *ast.IntegerNode:
190+
return n.GetToken().Value, nil
191+
case *ast.FloatNode:
192+
return n.GetToken().Value, nil
193+
case *ast.BoolNode:
194+
return n.GetToken().Value, nil
195+
default:
196+
return "", fmt.Errorf("node is not a scalar value (type: %T)", node)
160197
}
161-
162-
return "", fmt.Errorf("node is not a scalar value (type: %T)", node)
163198
}
164199

165200
// unwrapNode unwraps AnchorNode and AliasNode to get the actual value node.
@@ -179,29 +214,32 @@ func unwrapNode(node ast.Node) ast.Node {
179214
// setNestedValue sets a value at a nested path, creating nodes as needed.
180215
func setNestedValue(mapping *ast.MappingNode, keys []string, value string) error {
181216
current := mapping
217+
// Infer indent step once from the root mapping
218+
indentStep := getIndentStep(mapping)
182219

183220
for i, k := range keys {
184221
keyPart, arrayIdx := parseArrayIndex(k)
185222

186223
var valueNode ast.Node
187-
var mappingValueNode *ast.MappingValueNode
188-
189224
for _, item := range current.Values {
190225
if keyNode, ok := item.Key.(*ast.StringNode); ok && keyNode.Value == keyPart {
191226
valueNode = item.Value
192-
mappingValueNode = item
193227
break
194228
}
195229
}
196230

197231
// If key not found, create it
198232
if valueNode == nil {
233+
// Cannot create missing array indices
234+
if arrayIdx != nil {
235+
return fmt.Errorf("cannot set value: key %q does not exist (array indices must reference existing sequences)", keyPart)
236+
}
199237
if i == len(keys)-1 {
200238
// Last key - create scalar value
201239
return addKeyValue(current, k, value)
202240
}
203-
// Create intermediate mapping
204-
newMapping, err := addKeyMapping(current, keyPart)
241+
// Create intermediate mapping with the inferred indent step
242+
newMapping, err := addKeyMapping(current, keyPart, indentStep)
205243
if err != nil {
206244
return err
207245
}
@@ -225,9 +263,6 @@ func setNestedValue(mapping *ast.MappingNode, keys []string, value string) error
225263
}
226264

227265
if i == len(keys)-1 {
228-
if mappingValueNode != nil && arrayIdx == nil {
229-
return setNodeValue(unwrapNode(mappingValueNode.Value), value)
230-
}
231266
return setNodeValue(valueNode, value)
232267
}
233268

@@ -318,137 +353,88 @@ func getMappingIndent(mapping *ast.MappingNode) int {
318353
return 0
319354
}
320355

321-
// addKeyValue adds a new key-value pair to a mapping.
322-
func addKeyValue(mapping *ast.MappingNode, key string, value string) error {
323-
indent := getMappingIndent(mapping)
324-
325-
keyTok := &token.Token{
326-
Type: token.StringType,
327-
Value: key,
328-
Origin: key,
356+
// makeToken creates a token with position information.
357+
// Column is 1-indexed, so indent 4 means column 5.
358+
func makeToken(tokenType token.Type, value string, indent int) *token.Token {
359+
return &token.Token{
360+
Type: tokenType,
361+
Value: value,
362+
Origin: value,
329363
Position: &token.Position{
330364
IndentNum: indent,
331365
IndentLevel: 0,
332366
Column: indent + 1,
333367
},
334368
}
369+
}
335370

336-
colonTok := &token.Token{
337-
Type: token.MappingValueType,
338-
Value: ":",
339-
Origin: ":",
371+
// makeEmptyMapping creates an empty mapping node with the given indent.
372+
func makeEmptyMapping(indent int) *ast.MappingNode {
373+
startTok := &token.Token{
374+
Type: token.MappingStartType,
340375
Position: &token.Position{
341376
IndentNum: indent,
342377
IndentLevel: 0,
343-
Column: indent + len(key) + 1,
378+
Column: indent + 1,
344379
},
345380
}
381+
return ast.Mapping(startTok, false)
382+
}
346383

347-
valTok := &token.Token{
348-
Type: token.StringType,
349-
Value: value,
350-
Origin: value,
351-
Position: &token.Position{
352-
IndentNum: indent,
353-
IndentLevel: 0,
354-
Column: indent + len(key) + 3,
355-
},
384+
// getIndentStep infers the indent step size from the document.
385+
// Returns the number of spaces to add when creating a child level.
386+
func getIndentStep(mapping *ast.MappingNode) int {
387+
// Try to infer from a nested mapping value
388+
for _, item := range mapping.Values {
389+
if childMapping, ok := unwrapNode(item.Value).(*ast.MappingNode); ok {
390+
if len(childMapping.Values) > 0 {
391+
parentIndent := item.Key.GetToken().Position.IndentNum
392+
childIndent := childMapping.Values[0].Key.GetToken().Position.IndentNum
393+
if childIndent > parentIndent {
394+
return childIndent - parentIndent
395+
}
396+
}
397+
}
356398
}
399+
// Default to 2 spaces if we can't infer
400+
return 2
401+
}
357402

358-
keyNode := &ast.StringNode{
359-
BaseNode: &ast.BaseNode{},
360-
Token: keyTok,
361-
Value: key,
362-
}
403+
// addKeyValue adds a new key-value pair to a mapping.
404+
func addKeyValue(mapping *ast.MappingNode, key string, value string) error {
405+
indent := getMappingIndent(mapping)
363406

364-
valNode := &ast.StringNode{
365-
BaseNode: &ast.BaseNode{},
366-
Token: valTok,
367-
Value: value,
368-
}
407+
keyTok := makeToken(token.StringType, key, indent)
408+
colonTok := makeToken(token.MappingValueType, ":", indent)
409+
valTok := makeToken(token.StringType, value, indent)
369410

370-
newValue := &ast.MappingValueNode{
371-
BaseNode: &ast.BaseNode{},
372-
Start: colonTok,
373-
Key: keyNode,
374-
Value: valNode,
375-
}
411+
keyNode := ast.String(keyTok)
412+
valNode := ast.String(valTok)
413+
newValue := ast.MappingValue(colonTok, keyNode, valNode)
376414

377415
mapping.Values = append(mapping.Values, newValue)
378416
return nil
379417
}
380418

381419
// addKeyMapping adds a new key with an empty mapping value.
382-
func addKeyMapping(mapping *ast.MappingNode, key string) (*ast.MappingNode, error) {
420+
func addKeyMapping(mapping *ast.MappingNode, key string, indentStep int) (*ast.MappingNode, error) {
383421
indent := getMappingIndent(mapping)
422+
childIndent := indent + indentStep
384423

385-
// Calculate child indent (2 more than current)
386-
childIndent := indent + 2
387-
388-
keyTok := &token.Token{
389-
Type: token.StringType,
390-
Value: key,
391-
Origin: key,
392-
Position: &token.Position{
393-
IndentNum: indent,
394-
IndentLevel: 0,
395-
Column: indent + 1,
396-
},
397-
}
398-
399-
colonTok := &token.Token{
400-
Type: token.MappingValueType,
401-
Value: ":",
402-
Origin: ":",
403-
Position: &token.Position{
404-
IndentNum: indent,
405-
IndentLevel: 0,
406-
Column: indent + len(key) + 1,
407-
},
408-
}
409-
410-
keyNode := &ast.StringNode{
411-
BaseNode: &ast.BaseNode{},
412-
Token: keyTok,
413-
Value: key,
414-
}
424+
keyTok := makeToken(token.StringType, key, indent)
425+
colonTok := makeToken(token.MappingValueType, ":", indent)
415426

416-
newMapping := &ast.MappingNode{
417-
BaseNode: &ast.BaseNode{},
418-
Values: []*ast.MappingValueNode{},
419-
Start: &token.Token{
420-
Type: token.MappingStartType,
421-
Position: &token.Position{
422-
IndentNum: childIndent,
423-
IndentLevel: 0,
424-
},
425-
},
426-
}
427-
428-
newValue := &ast.MappingValueNode{
429-
BaseNode: &ast.BaseNode{},
430-
Start: colonTok,
431-
Key: keyNode,
432-
Value: newMapping,
433-
}
427+
keyNode := ast.String(keyTok)
428+
newMapping := makeEmptyMapping(childIndent)
429+
newValue := ast.MappingValue(colonTok, keyNode, newMapping)
434430

435431
mapping.Values = append(mapping.Values, newValue)
436432
return newMapping, nil
437433
}
438434

439435
// CreateEmptyValuesFile creates an empty values file with an optional header comment.
440436
func CreateEmptyValuesFile(headerComment string) *ValuesFile {
441-
mapping := &ast.MappingNode{
442-
BaseNode: &ast.BaseNode{},
443-
Values: []*ast.MappingValueNode{},
444-
Start: &token.Token{
445-
Type: token.MappingStartType,
446-
Position: &token.Position{
447-
IndentNum: 0,
448-
IndentLevel: 0,
449-
},
450-
},
451-
}
437+
mapping := makeEmptyMapping(0)
452438

453439
doc := &ast.DocumentNode{
454440
BaseNode: &ast.BaseNode{},

0 commit comments

Comments
 (0)