Skip to content

Commit 5f5224d

Browse files
committed
feat: Implement upsert functionality in overlay actions
1 parent dbbb19d commit 5f5224d

41 files changed

Lines changed: 2928 additions & 1042 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,133 @@ $.person.*~
280280

281281
---
282282

283+
## Overlay Support
284+
285+
This library includes support for YAML overlays, which allow you to apply structured modifications to YAML documents using JSONPath expressions.
286+
287+
### Basic Overlay Usage
288+
289+
Overlays are defined in YAML format and specify actions to apply to a target document:
290+
291+
```yaml
292+
overlay: "1.0.0"
293+
info:
294+
title: My Overlay
295+
version: "1.0"
296+
actions:
297+
- target: $.spec.replicas
298+
update: 3
299+
```
300+
301+
```go
302+
package main
303+
304+
import (
305+
"fmt"
306+
"github.com/pb33f/jsonpath/pkg/overlay"
307+
"go.yaml.in/yaml/v4"
308+
)
309+
310+
func main() {
311+
// Parse the overlay
312+
ov, _ := overlay.ParseOverlay(overlayYAML)
313+
314+
// Apply to a document
315+
result, _ := ov.Apply(documentNode)
316+
}
317+
```
318+
319+
### Upsert Action
320+
321+
The `upsert` action combines update and insert behavior. When `upsert: true` is set:
322+
323+
- If the target path exists, the value is updated (same as `update`)
324+
- If the target path doesn't exist, the path is created and the value is set
325+
326+
**Example: Create nested paths**
327+
328+
```yaml
329+
# Input document
330+
spec:
331+
existing: value
332+
333+
# Overlay
334+
overlay: "1.0.0"
335+
actions:
336+
- target: $.spec.config.nested.key
337+
update: created
338+
upsert: true
339+
340+
# Result
341+
spec:
342+
existing: value
343+
config:
344+
nested:
345+
key: created
346+
```
347+
348+
**Example: Update existing values**
349+
350+
```yaml
351+
# Input document
352+
spec:
353+
existing: value
354+
355+
# Overlay
356+
overlay: "1.0.0"
357+
actions:
358+
- target: $.spec.existing
359+
update: updated
360+
upsert: true
361+
362+
# Result
363+
spec:
364+
existing: updated
365+
```
366+
367+
**Example: Array elements**
368+
369+
```yaml
370+
# Input document
371+
items:
372+
- name: first
373+
374+
# Overlay
375+
overlay: "1.0.0"
376+
actions:
377+
- target: $.items[0].name
378+
update: updated
379+
upsert: true
380+
381+
# Result
382+
items:
383+
- name: updated
384+
```
385+
386+
### Supported Path Types for Upsert
387+
388+
Upsert works with **singular paths** - paths that resolve to exactly one location:
389+
390+
| Path Type | Example | Behavior |
391+
|-----------|---------|----------|
392+
| Member name | `$.foo.bar` | Creates nested maps as needed |
393+
| Array index | `$.items[0]` | Creates arrays and sets at index |
394+
| Combined | `$.a.b[2].c` | Creates nested structures |
395+
396+
### Unsupported Path Types
397+
398+
The following path types **cannot** be used with upsert (will return an error):
399+
400+
| Path Type | Example | Reason |
401+
|-----------|---------|--------|
402+
| Wildcard | `$.*.foo` | Ambiguous - which child? |
403+
| Recursive descent | `$..foo` | Ambiguous location |
404+
| Filter | `$[?(@.x)]` | Query, not specific location |
405+
| Multiple selectors | `$['a','b']` | Multiple locations |
406+
| Slice | `$[0:5]` | Multiple locations |
407+
408+
---
409+
283410
## Standard RFC 9535 Features
284411

285412
This library fully implements RFC 9535, including:

pkg/jsonpath/jsonpath.go

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,63 @@
11
package jsonpath
22

33
import (
4-
"fmt"
5-
"github.com/pb33f/jsonpath/pkg/jsonpath/config"
6-
"github.com/pb33f/jsonpath/pkg/jsonpath/token"
7-
"go.yaml.in/yaml/v4"
4+
"fmt"
5+
"github.com/pb33f/jsonpath/pkg/jsonpath/config"
6+
"github.com/pb33f/jsonpath/pkg/jsonpath/token"
7+
"go.yaml.in/yaml/v4"
88
)
99

1010
func NewPath(input string, opts ...config.Option) (*JSONPath, error) {
11-
tokenizer := token.NewTokenizer(input, opts...)
12-
tokens := tokenizer.Tokenize()
13-
for i := 0; i < len(tokens); i++ {
14-
if tokens[i].Token == token.ILLEGAL {
15-
return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], "unexpected token"))
16-
}
17-
}
18-
parser := newParserPrivate(tokenizer, tokens, opts...)
19-
err := parser.parse()
20-
if err != nil {
21-
return nil, err
22-
}
23-
return parser, nil
11+
tokenizer := token.NewTokenizer(input, opts...)
12+
tokens := tokenizer.Tokenize()
13+
for i := 0; i < len(tokens); i++ {
14+
if tokens[i].Token == token.ILLEGAL {
15+
return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], "unexpected token"))
16+
}
17+
}
18+
parser := newParserPrivate(tokenizer, tokens, opts...)
19+
err := parser.parse()
20+
if err != nil {
21+
return nil, err
22+
}
23+
return parser, nil
2424
}
2525

2626
func (p *JSONPath) Query(root *yaml.Node) []*yaml.Node {
27-
return p.ast.Query(root, root)
27+
return p.ast.Query(root, root)
2828
}
2929

3030
func (p *JSONPath) String() string {
31-
if p == nil {
32-
return ""
33-
}
34-
return p.ast.ToString()
31+
if p == nil {
32+
return ""
33+
}
34+
return p.ast.ToString()
35+
}
36+
37+
func (p *JSONPath) IsSingular() bool {
38+
if p == nil {
39+
return false
40+
}
41+
return p.ast.isSingular()
42+
}
43+
44+
type SegmentInfo struct {
45+
Kind SegmentKind
46+
Key string
47+
Index int64
48+
HasIndex bool
49+
}
50+
51+
type SegmentKind int
52+
53+
const (
54+
SegmentKindMemberName SegmentKind = iota
55+
SegmentKindArrayIndex
56+
)
57+
58+
func (p *JSONPath) GetSegmentInfo() ([]SegmentInfo, error) {
59+
if p == nil {
60+
return nil, fmt.Errorf("nil path")
61+
}
62+
return p.ast.getSegmentInfo()
3563
}

0 commit comments

Comments
 (0)