Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,133 @@ $.person.*~

---

## Overlay Support

This library includes support for YAML overlays, which allow you to apply structured modifications to YAML documents using JSONPath expressions.

### Basic Overlay Usage

Overlays are defined in YAML format and specify actions to apply to a target document:

```yaml
overlay: "1.0.0"
info:
title: My Overlay
version: "1.0"
actions:
- target: $.spec.replicas
update: 3
```

```go
package main

import (
"fmt"
"github.com/pb33f/jsonpath/pkg/overlay"
"go.yaml.in/yaml/v4"
)

func main() {
// Parse the overlay
ov, _ := overlay.ParseOverlay(overlayYAML)

// Apply to a document
result, _ := ov.Apply(documentNode)
}
```

### Upsert Action

The `upsert` action combines update and insert behavior. When `upsert: true` is set:

- If the target path exists, the value is updated (same as `update`)
- If the target path doesn't exist, the path is created and the value is set

**Example: Create nested paths**

```yaml
# Input document
spec:
existing: value

# Overlay
overlay: "1.0.0"
actions:
- target: $.spec.config.nested.key
update: created
upsert: true

# Result
spec:
existing: value
config:
nested:
key: created
```

**Example: Update existing values**

```yaml
# Input document
spec:
existing: value

# Overlay
overlay: "1.0.0"
actions:
- target: $.spec.existing
update: updated
upsert: true

# Result
spec:
existing: updated
```

**Example: Array elements**

```yaml
# Input document
items:
- name: first

# Overlay
overlay: "1.0.0"
actions:
- target: $.items[0].name
update: updated
upsert: true

# Result
items:
- name: updated
```

### Supported Path Types for Upsert

Upsert works with **singular paths** - paths that resolve to exactly one location:

| Path Type | Example | Behavior |
|-----------|---------|----------|
| Member name | `$.foo.bar` | Creates nested maps as needed |
| Array index | `$.items[0]` | Creates arrays and sets at index |
| Combined | `$.a.b[2].c` | Creates nested structures |

### Unsupported Path Types

The following path types **cannot** be used with upsert (will return an error):

| Path Type | Example | Reason |
|-----------|---------|--------|
| Wildcard | `$.*.foo` | Ambiguous - which child? |
| Recursive descent | `$..foo` | Ambiguous location |
| Filter | `$[?(@.x)]` | Query, not specific location |
| Multiple selectors | `$['a','b']` | Multiple locations |
| Slice | `$[0:5]` | Multiple locations |

---

## Standard RFC 9535 Features

This library fully implements RFC 9535, including:
Expand Down
72 changes: 50 additions & 22 deletions pkg/jsonpath/jsonpath.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,63 @@
package jsonpath

import (
"fmt"
"github.com/pb33f/jsonpath/pkg/jsonpath/config"
"github.com/pb33f/jsonpath/pkg/jsonpath/token"
"go.yaml.in/yaml/v4"
"fmt"
"github.com/pb33f/jsonpath/pkg/jsonpath/config"
"github.com/pb33f/jsonpath/pkg/jsonpath/token"
"go.yaml.in/yaml/v4"
)

func NewPath(input string, opts ...config.Option) (*JSONPath, error) {
tokenizer := token.NewTokenizer(input, opts...)
tokens := tokenizer.Tokenize()
for i := 0; i < len(tokens); i++ {
if tokens[i].Token == token.ILLEGAL {
return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], "unexpected token"))
}
}
parser := newParserPrivate(tokenizer, tokens, opts...)
err := parser.parse()
if err != nil {
return nil, err
}
return parser, nil
tokenizer := token.NewTokenizer(input, opts...)
tokens := tokenizer.Tokenize()
for i := 0; i < len(tokens); i++ {
if tokens[i].Token == token.ILLEGAL {
return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], "unexpected token"))
}
}
parser := newParserPrivate(tokenizer, tokens, opts...)
err := parser.parse()
if err != nil {
return nil, err
}
return parser, nil
}

func (p *JSONPath) Query(root *yaml.Node) []*yaml.Node {
return p.ast.Query(root, root)
return p.ast.Query(root, root)
}

func (p *JSONPath) String() string {
if p == nil {
return ""
}
return p.ast.ToString()
if p == nil {
return ""
}
return p.ast.ToString()
}

func (p *JSONPath) IsSingular() bool {
if p == nil {
return false
}
return p.ast.isSingular()
}

type SegmentInfo struct {
Kind SegmentKind
Key string
Index int64
HasIndex bool
}

type SegmentKind int

const (
SegmentKindMemberName SegmentKind = iota
SegmentKindArrayIndex
)

func (p *JSONPath) GetSegmentInfo() ([]SegmentInfo, error) {
if p == nil {
return nil, fmt.Errorf("nil path")
}
return p.ast.getSegmentInfo()
}
Loading