Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
34477b2c34 | |||
d5c08d86f5 | |||
68127fe453 | |||
872f2dd46d | |||
4eed05c7c2 | |||
4640281fbf | |||
aba10267d1 | |||
fed140254b | |||
db92033642 | |||
1b0b198297 |
116
README.md
Normal file
116
README.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Big Chef
|
||||||
|
|
||||||
|
A Go-based tool for modifying XML, JSON, and text documents using XPath/JSONPath/Regex expressions and Lua transformations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Format Processing**:
|
||||||
|
- XML (XPath)
|
||||||
|
- JSON (JSONPath)
|
||||||
|
- Text (Regex)
|
||||||
|
- **Node Value Modification**: Update text values in XML elements, JSON properties or text matches
|
||||||
|
- **Attribute Manipulation**: Modify XML attributes, JSON object keys or regex capture groups
|
||||||
|
- **Conditional Logic**: Apply transformations based on document content
|
||||||
|
- **Complex Operations**:
|
||||||
|
- Mathematical calculations
|
||||||
|
- String manipulations
|
||||||
|
- Date conversions
|
||||||
|
- Structural changes
|
||||||
|
- Whole ass Lua environment
|
||||||
|
- **Error Handling**: Comprehensive error detection for:
|
||||||
|
- Invalid XML/JSON
|
||||||
|
- Malformed XPath/JSONPath
|
||||||
|
- Lua syntax errors
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### 1. Basic field modification
|
||||||
|
```xml
|
||||||
|
<!-- Input -->
|
||||||
|
<price>44.95</price>
|
||||||
|
|
||||||
|
<!-- Command -->
|
||||||
|
chef -xml "//price" "v=v*2" input.xml
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<price>89.9</price>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Supports glob patterns
|
||||||
|
```xml
|
||||||
|
chef -xml "//price" "v=v*2" data/**.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Attribute Update
|
||||||
|
```xml
|
||||||
|
<!-- Input -->
|
||||||
|
<item price="10.50"/>
|
||||||
|
|
||||||
|
<!-- Command -->
|
||||||
|
chef -xml "//item/@price" "v=v*2" input.xml
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<item price="21"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. JSONPath Transformation
|
||||||
|
```json
|
||||||
|
// Input
|
||||||
|
{
|
||||||
|
"products": [
|
||||||
|
{"name": "Widget", "price": 19.99},
|
||||||
|
{"name": "Gadget", "price": 29.99}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command
|
||||||
|
chef -json "$.products[*].price" "v=v*0.75" input.json
|
||||||
|
|
||||||
|
// Output
|
||||||
|
{
|
||||||
|
"products": [
|
||||||
|
{"name": "Widget", "price": 14.99},
|
||||||
|
{"name": "Gadget", "price": 22.49}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Regex Text Replacement
|
||||||
|
Regex works slightly differently, up to 12 match groups are provided as v1..v12 and s1..s12 for numbers and strings respectively.
|
||||||
|
A special shorthand "!num" is also provided that simply expands to `(\d*\.?\d+)`.
|
||||||
|
```xml
|
||||||
|
<!-- Input -->
|
||||||
|
<description>Price: $15.00 Special Offer</description>
|
||||||
|
|
||||||
|
<!-- Command -->
|
||||||
|
chef "Price: $!num Special Offer" "v1 = v1 * 0.92" input.xml
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<description>Price: $13.80 Special Offer</description>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Conditional Transformation
|
||||||
|
```xml
|
||||||
|
<!-- Input -->
|
||||||
|
<item stock="5" price="10.00"/>
|
||||||
|
|
||||||
|
<!-- Command -->
|
||||||
|
chef -xml "//item" "if tonumber(v.stock) > 0 then v.price = v.price * 0.8 end" input.xml
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<item stock="5" price="8.00"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o chef main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Process XML file
|
||||||
|
./chef -xml "//price" "v=v*1.2" input.xml
|
||||||
|
|
||||||
|
# Process JSON file
|
||||||
|
./chef -json "$.prices[*]" "v=v*0.9" input.json
|
||||||
|
```
|
4
go.mod
4
go.mod
@@ -3,6 +3,7 @@ module modify
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PaesslerAG/jsonpath v0.1.1
|
||||||
github.com/antchfx/xmlquery v1.4.4
|
github.com/antchfx/xmlquery v1.4.4
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||||
github.com/yuin/gopher-lua v1.1.1
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
@@ -10,11 +11,8 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
|
||||||
github.com/antchfx/xpath v1.3.3 // indirect
|
github.com/antchfx/xpath v1.3.3 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
16
go.sum
16
go.sum
@@ -9,21 +9,9 @@ github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
|
|||||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
@@ -92,7 +80,3 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
|
32
main.go
32
main.go
@@ -19,19 +19,12 @@ type GlobalStats struct {
|
|||||||
FailedFiles int
|
FailedFiles int
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ModeRegex FileMode = "regex"
|
|
||||||
ModeXML FileMode = "xml"
|
|
||||||
ModeJSON FileMode = "json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var stats GlobalStats
|
var stats GlobalStats
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileModeFlag = flag.String("mode", "regex", "Processing mode: regex, xml, json")
|
jsonFlag = flag.Bool("json", false, "Process JSON files")
|
||||||
|
xmlFlag = flag.Bool("xml", false, "Process XML files")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -67,7 +60,7 @@ func main() {
|
|||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
|
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
fmt.Fprintf(os.Stderr, "%s mode requires %d arguments minimum\n", *fileModeFlag, 3)
|
log.Printf("At least %d arguments are required", 3)
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -101,20 +94,19 @@ func main() {
|
|||||||
|
|
||||||
// Create the processor based on mode
|
// Create the processor based on mode
|
||||||
var proc processor.Processor
|
var proc processor.Processor
|
||||||
switch *fileModeFlag {
|
switch {
|
||||||
case "regex":
|
case *xmlFlag:
|
||||||
proc = &processor.RegexProcessor{}
|
proc = &processor.XMLProcessor{}
|
||||||
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
|
logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
|
||||||
pattern, luaExpr, len(files))
|
pattern, luaExpr, len(files))
|
||||||
// case "xml":
|
case *jsonFlag:
|
||||||
// proc = &processor.XMLProcessor{}
|
|
||||||
// pattern = *xpathFlag
|
|
||||||
// logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
|
|
||||||
// pattern, luaExpr, len(files))
|
|
||||||
case "json":
|
|
||||||
proc = &processor.JSONProcessor{}
|
proc = &processor.JSONProcessor{}
|
||||||
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
|
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
|
||||||
pattern, luaExpr, len(files))
|
pattern, luaExpr, len(files))
|
||||||
|
default:
|
||||||
|
proc = &processor.RegexProcessor{}
|
||||||
|
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
|
||||||
|
pattern, luaExpr, len(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
@@ -7,7 +7,6 @@ import (
|
|||||||
"modify/processor/jsonpath"
|
"modify/processor/jsonpath"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
@@ -68,6 +67,7 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
return content, 0, 0, nil
|
return content, 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modCount := 0
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
|
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
|
||||||
|
|
||||||
@@ -85,10 +85,14 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
}
|
}
|
||||||
log.Printf("Converted node value to Lua: %v", node.Value)
|
log.Printf("Converted node value to Lua: %v", node.Value)
|
||||||
|
|
||||||
|
originalScript := luaExpr
|
||||||
|
fullScript := BuildLuaScript(luaExpr)
|
||||||
|
log.Printf("Original script: %q, Full script: %q", originalScript, fullScript)
|
||||||
|
|
||||||
// Execute Lua script
|
// Execute Lua script
|
||||||
log.Printf("Executing Lua script: %s", luaExpr)
|
log.Printf("Executing Lua script: %q", fullScript)
|
||||||
if err := L.DoString(luaExpr); err != nil {
|
if err := L.DoString(fullScript); err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error executing Lua %s: %v", luaExpr, err)
|
return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
|
||||||
}
|
}
|
||||||
log.Println("Lua script executed successfully.")
|
log.Println("Lua script executed successfully.")
|
||||||
|
|
||||||
@@ -99,51 +103,29 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
}
|
}
|
||||||
log.Printf("Retrieved modified value from Lua: %v", result)
|
log.Printf("Retrieved modified value from Lua: %v", result)
|
||||||
|
|
||||||
|
modified := false
|
||||||
|
modified = L.GetGlobal("modified").String() == "true"
|
||||||
|
if !modified {
|
||||||
|
log.Printf("No changes made to node at path: %s", node.Path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the modification to the JSON data
|
// Apply the modification to the JSON data
|
||||||
err = p.updateJSONValue(jsonData, node.Path, result)
|
err = p.updateJSONValue(jsonData, node.Path, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
|
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result)
|
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result)
|
||||||
|
modCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the modified JSON back to a string with same formatting
|
// Convert the modified JSON back to a string with same formatting
|
||||||
var jsonBytes []byte
|
var jsonBytes []byte
|
||||||
if indent, err := detectJsonIndentation(content); err == nil && indent != "" {
|
|
||||||
// Use detected indentation for output formatting
|
|
||||||
jsonBytes, err = json.MarshalIndent(jsonData, "", indent)
|
|
||||||
} else {
|
|
||||||
// Fall back to standard 2-space indent
|
|
||||||
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
return string(jsonBytes), modCount, matchCount, nil
|
||||||
// We changed all the nodes trust me bro
|
|
||||||
return string(jsonBytes), len(nodes), len(nodes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectJsonIndentation tries to determine the indentation used in the original JSON
|
|
||||||
func detectJsonIndentation(content string) (string, error) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
if len(lines) < 2 {
|
|
||||||
return "", fmt.Errorf("not enough lines to detect indentation")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for the first indented line
|
|
||||||
for i := 1; i < len(lines); i++ {
|
|
||||||
line := lines[i]
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate leading whitespace
|
|
||||||
indent := line[:len(line)-len(trimmed)]
|
|
||||||
if len(indent) > 0 {
|
|
||||||
return indent, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no indentation detected")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// / Selects from the root node
|
// / Selects from the root node
|
||||||
@@ -165,6 +147,52 @@ func detectJsonIndentation(content string) (string, error) {
|
|||||||
|
|
||||||
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
||||||
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
||||||
|
// Special handling for root node
|
||||||
|
if path == "$" {
|
||||||
|
// For the root node, we'll copy the value to the jsonData reference
|
||||||
|
// This is a special case since we can't directly replace the interface{} variable
|
||||||
|
|
||||||
|
// We need to handle different types of root elements
|
||||||
|
switch rootValue := newValue.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// For objects, we need to copy over all keys
|
||||||
|
rootMap, ok := jsonData.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// If the original wasn't a map, completely replace it with the new map
|
||||||
|
// This is handled by the jsonpath.Set function
|
||||||
|
return jsonpath.Set(jsonData, path, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the original map
|
||||||
|
for k := range rootMap {
|
||||||
|
delete(rootMap, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all keys from the new map
|
||||||
|
for k, v := range rootValue {
|
||||||
|
rootMap[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
// For arrays, we need to handle similarly
|
||||||
|
rootArray, ok := jsonData.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
// If the original wasn't an array, use jsonpath.Set
|
||||||
|
return jsonpath.Set(jsonData, path, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear and recreate the array
|
||||||
|
*&rootArray = rootValue
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For other types, use jsonpath.Set
|
||||||
|
return jsonpath.Set(jsonData, path, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-root paths, use the regular Set method
|
||||||
err := jsonpath.Set(jsonData, path, newValue)
|
err := jsonpath.Set(jsonData, path, newValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
|
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -138,10 +138,6 @@ func Set(data interface{}, path string, value interface{}) error {
|
|||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(steps) <= 1 {
|
|
||||||
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := false
|
success := false
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -157,10 +153,6 @@ func SetAll(data interface{}, path string, value interface{}) error {
|
|||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(steps) <= 1 {
|
|
||||||
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
success := false
|
success := false
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -178,17 +170,20 @@ func setWithPath(node interface{}, steps []JSONStep, success *bool, value interf
|
|||||||
// Skip root step
|
// Skip root step
|
||||||
actualSteps := steps
|
actualSteps := steps
|
||||||
if len(steps) > 0 && steps[0].Type == RootStep {
|
if len(steps) > 0 && steps[0].Type == RootStep {
|
||||||
if len(steps) == 1 {
|
|
||||||
return fmt.Errorf("cannot set root node; the provided path %q is invalid", currentPath)
|
|
||||||
}
|
|
||||||
actualSteps = steps[1:]
|
actualSteps = steps[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the first step
|
// If we have no steps left, we're setting the root value
|
||||||
if len(actualSteps) == 0 {
|
if len(actualSteps) == 0 {
|
||||||
return fmt.Errorf("cannot set root node; no steps provided for path %q", currentPath)
|
// For the root node, we need to handle it differently depending on what's passed in
|
||||||
|
// since we can't directly replace the interface{} variable
|
||||||
|
|
||||||
|
// We'll signal success and let the JSONProcessor handle updating the root
|
||||||
|
*success = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process the first step
|
||||||
step := actualSteps[0]
|
step := actualSteps[0]
|
||||||
remainingSteps := actualSteps[1:]
|
remainingSteps := actualSteps[1:]
|
||||||
isLastStep := len(remainingSteps) == 0
|
isLastStep := len(remainingSteps) == 0
|
||||||
|
@@ -355,14 +355,14 @@ func TestSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("setting on root should fail", func(t *testing.T) {
|
t.Run("setting on root should not fail (anymore)", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"name": "John",
|
"name": "John",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Set(data, "$", "Jane")
|
err := Set(data, "$", "Jane")
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Errorf("Set() returned no error, expected error for setting on root")
|
t.Errorf("Set() returned error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,6 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
@@ -81,6 +80,8 @@ func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
|||||||
return lua.LBool(v), nil
|
return lua.LBool(v), nil
|
||||||
case float64:
|
case float64:
|
||||||
return lua.LNumber(v), nil
|
return lua.LNumber(v), nil
|
||||||
|
case nil:
|
||||||
|
return lua.LNil, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported data type: %T", data)
|
return nil, fmt.Errorf("unsupported data type: %T", data)
|
||||||
}
|
}
|
||||||
@@ -89,29 +90,31 @@ func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
|||||||
// FromLua converts a Lua table to a struct or map recursively
|
// FromLua converts a Lua table to a struct or map recursively
|
||||||
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
||||||
switch v := luaValue.(type) {
|
switch v := luaValue.(type) {
|
||||||
|
// Well shit...
|
||||||
|
// Tables in lua are both maps and arrays
|
||||||
|
// As arrays they are ordered and as maps, obviously, not
|
||||||
|
// So when we parse them to a go map we fuck up the order for arrays
|
||||||
|
// We have to find a better way....
|
||||||
case *lua.LTable:
|
case *lua.LTable:
|
||||||
result := make(map[string]interface{})
|
isArray, err := IsLuaTableArray(L, v)
|
||||||
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
||||||
result[key.String()], _ = FromLua(L, value)
|
|
||||||
})
|
|
||||||
// This may be a bit wasteful...
|
|
||||||
// Hopefully it won't run often enough to matter
|
|
||||||
isArray := true
|
|
||||||
for key := range result {
|
|
||||||
_, err := strconv.Atoi(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
isArray = false
|
return nil, err
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if isArray {
|
if isArray {
|
||||||
list := make([]interface{}, 0, len(result))
|
result := make([]interface{}, 0)
|
||||||
for _, value := range result {
|
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||||
list = append(list, value)
|
converted, _ := FromLua(L, value)
|
||||||
}
|
result = append(result, converted)
|
||||||
return list, nil
|
})
|
||||||
}
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
} else {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||||
|
converted, _ := FromLua(L, value)
|
||||||
|
result[key.String()] = converted
|
||||||
|
})
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
case lua.LString:
|
case lua.LString:
|
||||||
return string(v), nil
|
return string(v), nil
|
||||||
case lua.LBool:
|
case lua.LBool:
|
||||||
@@ -123,13 +126,34 @@ func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
|
||||||
|
L.SetGlobal("table_to_check", v)
|
||||||
|
|
||||||
|
// Use our predefined helper function from InitLuaHelpers
|
||||||
|
err := L.DoString(`is_array = isArray(table_to_check)`)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error determining if table is array: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the result of our Lua function
|
||||||
|
isArray := L.GetGlobal("is_array")
|
||||||
|
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
|
||||||
|
if !lua.LVIsFalse(isArray) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitLuaHelpers initializes common Lua helper functions
|
// InitLuaHelpers initializes common Lua helper functions
|
||||||
func InitLuaHelpers(L *lua.LState) error {
|
func InitLuaHelpers(L *lua.LState) error {
|
||||||
helperScript := `
|
helperScript := `
|
||||||
-- Custom Lua helpers for math operations
|
-- Custom Lua helpers for math operations
|
||||||
function min(a, b) return math.min(a, b) end
|
function min(a, b) return math.min(a, b) end
|
||||||
function max(a, b) return math.max(a, b) end
|
function max(a, b) return math.max(a, b) end
|
||||||
function round(x) return math.floor(x + 0.5) end
|
function round(x, n)
|
||||||
|
if n == nil then n = 0 end
|
||||||
|
return math.floor(x * 10^n + 0.5) / 10^n
|
||||||
|
end
|
||||||
function floor(x) return math.floor(x) end
|
function floor(x) return math.floor(x) end
|
||||||
function ceil(x) return math.ceil(x) end
|
function ceil(x) return math.ceil(x) end
|
||||||
function upper(s) return string.upper(s) end
|
function upper(s) return string.upper(s) end
|
||||||
@@ -149,6 +173,22 @@ end
|
|||||||
function is_number(str)
|
function is_number(str)
|
||||||
return tonumber(str) ~= nil
|
return tonumber(str) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function isArray(t)
|
||||||
|
if type(t) ~= "table" then return false end
|
||||||
|
local max = 0
|
||||||
|
local count = 0
|
||||||
|
for k, _ in pairs(t) do
|
||||||
|
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
max = math.max(max, k)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return max == count
|
||||||
|
end
|
||||||
|
|
||||||
|
modified = false
|
||||||
`
|
`
|
||||||
if err := L.DoString(helperScript); err != nil {
|
if err := L.DoString(helperScript); err != nil {
|
||||||
return fmt.Errorf("error loading helper functions: %v", err)
|
return fmt.Errorf("error loading helper functions: %v", err)
|
||||||
@@ -167,8 +207,7 @@ func LimitString(s string, maxLen int) string {
|
|||||||
return s[:maxLen-3] + "..."
|
return s[:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildLuaScript prepares a Lua expression from shorthand notation
|
func PrependLuaAssignment(luaExpr string) string {
|
||||||
func BuildLuaScript(luaExpr string) string {
|
|
||||||
// Auto-prepend v1 for expressions starting with operators
|
// Auto-prepend v1 for expressions starting with operators
|
||||||
if strings.HasPrefix(luaExpr, "*") ||
|
if strings.HasPrefix(luaExpr, "*") ||
|
||||||
strings.HasPrefix(luaExpr, "/") ||
|
strings.HasPrefix(luaExpr, "/") ||
|
||||||
@@ -186,10 +225,30 @@ func BuildLuaScript(luaExpr string) string {
|
|||||||
if !strings.Contains(luaExpr, "=") {
|
if !strings.Contains(luaExpr, "=") {
|
||||||
luaExpr = "v1 = " + luaExpr
|
luaExpr = "v1 = " + luaExpr
|
||||||
}
|
}
|
||||||
|
|
||||||
return luaExpr
|
return luaExpr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildLuaScript prepares a Lua expression from shorthand notation
|
||||||
|
func BuildLuaScript(luaExpr string) string {
|
||||||
|
luaExpr = PrependLuaAssignment(luaExpr)
|
||||||
|
|
||||||
|
// This allows the user to specify whether or not they modified a value
|
||||||
|
// If they do nothing we assume they did modify (no return at all)
|
||||||
|
// If they return before our return then they themselves specify what they did
|
||||||
|
// If nothing is returned lua assumes nil
|
||||||
|
// So we can say our value was modified if the return value is either nil or true
|
||||||
|
// If the return value is false then the user wants to keep the original
|
||||||
|
fullScript := fmt.Sprintf(`
|
||||||
|
function run()
|
||||||
|
%s
|
||||||
|
end
|
||||||
|
local res = run()
|
||||||
|
modified = res == nil or res
|
||||||
|
`, luaExpr)
|
||||||
|
|
||||||
|
return fullScript
|
||||||
|
}
|
||||||
|
|
||||||
// Max returns the maximum of two integers
|
// Max returns the maximum of two integers
|
||||||
func Max(a, b int) int {
|
func Max(a, b int) int {
|
||||||
if a > b {
|
if a > b {
|
||||||
|
@@ -35,10 +35,11 @@ func TestBuildLuaScript(t *testing.T) {
|
|||||||
{"v1 * 2", "v1 = v1 * 2"},
|
{"v1 * 2", "v1 = v1 * 2"},
|
||||||
{"v1 * v2", "v1 = v1 * v2"},
|
{"v1 * v2", "v1 = v1 * v2"},
|
||||||
{"v1 / v2", "v1 = v1 / v2"},
|
{"v1 / v2", "v1 = v1 / v2"},
|
||||||
|
{"12", "v1 = 12"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
result := BuildLuaScript(c.input)
|
result := PrependLuaAssignment(c.input)
|
||||||
if result != c.expected {
|
if result != c.expected {
|
||||||
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
|
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
|
||||||
}
|
}
|
||||||
|
98
processor/xpath/xpath.go
Normal file
98
processor/xpath/xpath.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package xpath
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// XPathStep represents a single step in an XPath expression
|
||||||
|
type XPathStep struct {
|
||||||
|
Type StepType
|
||||||
|
Name string
|
||||||
|
Predicate *Predicate
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepType defines the type of XPath step
|
||||||
|
type StepType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RootStep represents the root step (/)
|
||||||
|
RootStep StepType = iota
|
||||||
|
// ChildStep represents a child element step (element)
|
||||||
|
ChildStep
|
||||||
|
// RecursiveDescentStep represents a recursive descent step (//)
|
||||||
|
RecursiveDescentStep
|
||||||
|
// WildcardStep represents a wildcard step (*)
|
||||||
|
WildcardStep
|
||||||
|
// PredicateStep represents a predicate condition step ([...])
|
||||||
|
PredicateStep
|
||||||
|
)
|
||||||
|
|
||||||
|
// PredicateType defines the type of XPath predicate
|
||||||
|
type PredicateType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IndexPredicate represents an index predicate [n]
|
||||||
|
IndexPredicate PredicateType = iota
|
||||||
|
// LastPredicate represents a last() function predicate
|
||||||
|
LastPredicate
|
||||||
|
// LastMinusPredicate represents a last()-n predicate
|
||||||
|
LastMinusPredicate
|
||||||
|
// PositionPredicate represents position()-based predicates
|
||||||
|
PositionPredicate
|
||||||
|
// AttributeExistsPredicate represents [@attr] predicate
|
||||||
|
AttributeExistsPredicate
|
||||||
|
// AttributeEqualsPredicate represents [@attr='value'] predicate
|
||||||
|
AttributeEqualsPredicate
|
||||||
|
// ComparisonPredicate represents element comparison predicates
|
||||||
|
ComparisonPredicate
|
||||||
|
)
|
||||||
|
|
||||||
|
// Predicate represents a condition in XPath
|
||||||
|
type Predicate struct {
|
||||||
|
Type PredicateType
|
||||||
|
Index int
|
||||||
|
Offset int
|
||||||
|
Attribute string
|
||||||
|
Value string
|
||||||
|
Expression string
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLNode represents a node in the result set with its value and path
|
||||||
|
type XMLNode struct {
|
||||||
|
Value interface{}
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseXPath parses an XPath expression into a series of steps
|
||||||
|
func ParseXPath(path string) ([]XPathStep, error) {
|
||||||
|
if path == "" {
|
||||||
|
return nil, errors.New("empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just a placeholder implementation for the tests
|
||||||
|
// The actual implementation would parse the XPath expression
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves nodes from XML data using an XPath expression
|
||||||
|
func Get(data interface{}, path string) ([]XMLNode, error) {
|
||||||
|
if data == "" {
|
||||||
|
return nil, errors.New("empty XML data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just a placeholder implementation for the tests
|
||||||
|
// The actual implementation would evaluate the XPath against the XML
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set updates a node in the XML data using an XPath expression
|
||||||
|
func Set(xmlData string, path string, value interface{}) (string, error) {
|
||||||
|
// This is just a placeholder implementation for the tests
|
||||||
|
// The actual implementation would modify the XML based on the XPath
|
||||||
|
return "", errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAll updates all nodes matching an XPath expression in the XML data
|
||||||
|
func SetAll(xmlData string, path string, value interface{}) (string, error) {
|
||||||
|
// This is just a placeholder implementation for the tests
|
||||||
|
// The actual implementation would modify all matching nodes
|
||||||
|
return "", errors.New("not implemented")
|
||||||
|
}
|
545
processor/xpath/xpath_test.go
Normal file
545
processor/xpath/xpath_test.go
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
package xpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XML test data as a string for our tests
|
||||||
|
var testXML = `
|
||||||
|
<store>
|
||||||
|
<book category="fiction">
|
||||||
|
<title lang="en">The Fellowship of the Ring</title>
|
||||||
|
<author>J.R.R. Tolkien</author>
|
||||||
|
<year>1954</year>
|
||||||
|
<price>22.99</price>
|
||||||
|
</book>
|
||||||
|
<book category="fiction">
|
||||||
|
<title lang="en">The Two Towers</title>
|
||||||
|
<author>J.R.R. Tolkien</author>
|
||||||
|
<year>1954</year>
|
||||||
|
<price>23.45</price>
|
||||||
|
</book>
|
||||||
|
<book category="technical">
|
||||||
|
<title lang="en">Learning XML</title>
|
||||||
|
<author>Erik T. Ray</author>
|
||||||
|
<year>2003</year>
|
||||||
|
<price>39.95</price>
|
||||||
|
</book>
|
||||||
|
<bicycle>
|
||||||
|
<color>red</color>
|
||||||
|
<price>199.95</price>
|
||||||
|
</bicycle>
|
||||||
|
</store>
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
steps []XPathStep
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "/store/bicycle/color",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "bicycle"},
|
||||||
|
{Type: ChildStep, Name: "color"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "//price",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: RecursiveDescentStep, Name: "price"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book/*",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: WildcardStep},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book[1]/title",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{Type: IndexPredicate, Index: 1}},
|
||||||
|
{Type: ChildStep, Name: "title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "//title[@lang]",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: RecursiveDescentStep, Name: "title"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{Type: AttributeExistsPredicate, Attribute: "lang"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "//title[@lang='en']",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: RecursiveDescentStep, Name: "title"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{
|
||||||
|
Type: AttributeEqualsPredicate,
|
||||||
|
Attribute: "lang",
|
||||||
|
Value: "en",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book[price>35.00]/title",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{
|
||||||
|
Type: ComparisonPredicate,
|
||||||
|
Expression: "price>35.00",
|
||||||
|
}},
|
||||||
|
{Type: ChildStep, Name: "title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book[last()]",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{Type: LastPredicate}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book[last()-1]",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{
|
||||||
|
Type: LastMinusPredicate,
|
||||||
|
Offset: 1,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/store/book[position()<3]",
|
||||||
|
steps: []XPathStep{
|
||||||
|
{Type: RootStep},
|
||||||
|
{Type: ChildStep, Name: "store"},
|
||||||
|
{Type: ChildStep, Name: "book"},
|
||||||
|
{Type: PredicateStep, Predicate: &Predicate{
|
||||||
|
Type: PositionPredicate,
|
||||||
|
Expression: "position()<3",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "invalid/path",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
steps, err := ParseXPath(tt.path)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("ParseXPath() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
|
||||||
|
t.Errorf("ParseXPath() steps = %+v, want %+v", steps, tt.steps)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluator(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
expected []XMLNode
|
||||||
|
error bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple_element_access",
|
||||||
|
path: "/store/bicycle/color",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "red", Path: "/store/bicycle/color"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recursive_element_access",
|
||||||
|
path: "//price",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "22.99", Path: "/store/book[1]/price"},
|
||||||
|
{Value: "23.45", Path: "/store/book[2]/price"},
|
||||||
|
{Value: "39.95", Path: "/store/book[3]/price"},
|
||||||
|
{Value: "199.95", Path: "/store/bicycle/price"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard_element_access",
|
||||||
|
path: "/store/book[1]/*",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
{Value: "J.R.R. Tolkien", Path: "/store/book[1]/author"},
|
||||||
|
{Value: "1954", Path: "/store/book[1]/year"},
|
||||||
|
{Value: "22.99", Path: "/store/book[1]/price"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexed_element_access",
|
||||||
|
path: "/store/book[1]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute_exists_predicate",
|
||||||
|
path: "//title[@lang]",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
{Value: "The Two Towers", Path: "/store/book[2]/title"},
|
||||||
|
{Value: "Learning XML", Path: "/store/book[3]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute_equals_predicate",
|
||||||
|
path: "//title[@lang='en']",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
{Value: "The Two Towers", Path: "/store/book[2]/title"},
|
||||||
|
{Value: "Learning XML", Path: "/store/book[3]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value_comparison_predicate",
|
||||||
|
path: "/store/book[price>35.00]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "Learning XML", Path: "/store/book[3]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last_predicate",
|
||||||
|
path: "/store/book[last()]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "Learning XML", Path: "/store/book[3]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last_minus_predicate",
|
||||||
|
path: "/store/book[last()-1]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Two Towers", Path: "/store/book[2]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "position_predicate",
|
||||||
|
path: "/store/book[position()<3]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
{Value: "The Two Towers", Path: "/store/book[2]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all_elements",
|
||||||
|
path: "//*",
|
||||||
|
expected: []XMLNode{
|
||||||
|
// For brevity, we'll just check the count, not all values
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_index",
|
||||||
|
path: "/store/book[10]/title",
|
||||||
|
expected: []XMLNode{},
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexistent_element",
|
||||||
|
path: "/store/nonexistent",
|
||||||
|
expected: []XMLNode{},
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := Get(testXML, tt.path)
|
||||||
|
if err != nil {
|
||||||
|
if !tt.error {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for the "//*" test case
|
||||||
|
if tt.path == "//*" {
|
||||||
|
// Just check that we got multiple elements, not the specific count
|
||||||
|
if len(result) < 10 { // We expect at least 10 elements
|
||||||
|
t.Errorf("Expected multiple elements for '//*', got %d", len(result))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate both values and paths
|
||||||
|
for i, e := range tt.expected {
|
||||||
|
if i < len(result) {
|
||||||
|
if !reflect.DeepEqual(result[i].Value, e.Value) {
|
||||||
|
t.Errorf("Value at [%d]: got %v, expected %v", i, result[i].Value, e.Value)
|
||||||
|
}
|
||||||
|
if result[i].Path != e.Path {
|
||||||
|
t.Errorf("Path at [%d]: got %s, expected %s", i, result[i].Path, e.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEdgeCases(t *testing.T) {
|
||||||
|
t.Run("empty_data", func(t *testing.T) {
|
||||||
|
result, err := Get("", "/store/book")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for empty data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
t.Errorf("Expected empty result, got %v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty_path", func(t *testing.T) {
|
||||||
|
_, err := ParseXPath("")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for empty path")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid_xml", func(t *testing.T) {
|
||||||
|
_, err := Get("<invalid>xml", "/store")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid XML")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("current_node", func(t *testing.T) {
|
||||||
|
result, err := Get(testXML, "/store/book[1]/.")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("Expected 1 result, got %d", len(result))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attributes", func(t *testing.T) {
|
||||||
|
result, err := Get(testXML, "/store/book[1]/title/@lang")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 1 || result[0].Value != "en" {
|
||||||
|
t.Errorf("Expected 'en', got %v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWithPaths(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
expected []XMLNode
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple_element_access",
|
||||||
|
path: "/store/bicycle/color",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "red", Path: "/store/bicycle/color"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "indexed_element_access",
|
||||||
|
path: "/store/book[1]/title",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recursive_element_access",
|
||||||
|
path: "//price",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "22.99", Path: "/store/book[1]/price"},
|
||||||
|
{Value: "23.45", Path: "/store/book[2]/price"},
|
||||||
|
{Value: "39.95", Path: "/store/book[3]/price"},
|
||||||
|
{Value: "199.95", Path: "/store/bicycle/price"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute_access",
|
||||||
|
path: "/store/book[1]/title/@lang",
|
||||||
|
expected: []XMLNode{
|
||||||
|
{Value: "en", Path: "/store/book[1]/title/@lang"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := Get(testXML, tt.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if lengths match
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("Get() returned %d items, expected %d", len(result), len(tt.expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each expected item, find its match in the results and verify both value and path
|
||||||
|
for _, expected := range tt.expected {
|
||||||
|
found := false
|
||||||
|
for _, r := range result {
|
||||||
|
// First verify the value matches
|
||||||
|
if reflect.DeepEqual(r.Value, expected.Value) {
|
||||||
|
found = true
|
||||||
|
// Then verify the path matches
|
||||||
|
if r.Path != expected.Path {
|
||||||
|
t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSet(t *testing.T) {
|
||||||
|
t.Run("simple element", func(t *testing.T) {
|
||||||
|
xmlData := `<root><name>John</name></root>`
|
||||||
|
newXML, err := Set(xmlData, "/root/name", "Jane")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Set() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the change
|
||||||
|
result, err := Get(newXML, "/root/name")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 1 || result[0].Value != "Jane" {
|
||||||
|
t.Errorf("Set() failed: expected name to be 'Jane', got %v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attribute", func(t *testing.T) {
|
||||||
|
xmlData := `<root><element id="123"></element></root>`
|
||||||
|
newXML, err := Set(xmlData, "/root/element/@id", "456")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Set() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the change
|
||||||
|
result, err := Get(newXML, "/root/element/@id")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 1 || result[0].Value != "456" {
|
||||||
|
t.Errorf("Set() failed: expected id to be '456', got %v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("indexed element", func(t *testing.T) {
|
||||||
|
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
||||||
|
newXML, err := Set(xmlData, "/root/items/item[1]", "changed")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Set() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the change
|
||||||
|
result, err := Get(newXML, "/root/items/item[1]")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 1 || result[0].Value != "changed" {
|
||||||
|
t.Errorf("Set() failed: expected item to be 'changed', got %v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetAll(t *testing.T) {
|
||||||
|
t.Run("multiple elements", func(t *testing.T) {
|
||||||
|
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
||||||
|
newXML, err := SetAll(xmlData, "//item", "changed")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SetAll() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all items are changed
|
||||||
|
result, err := Get(newXML, "//item")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("Expected 2 results, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, node := range result {
|
||||||
|
if node.Value != "changed" {
|
||||||
|
t.Errorf("Item %d not changed, got %v", i+1, node.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attributes", func(t *testing.T) {
|
||||||
|
xmlData := `<root><item id="1"/><item id="2"/></root>`
|
||||||
|
newXML, err := SetAll(xmlData, "//item/@id", "new")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SetAll() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all attributes are changed
|
||||||
|
result, err := Get(newXML, "//item/@id")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get() returned error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("Expected 2 results, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, node := range result {
|
||||||
|
if node.Value != "new" {
|
||||||
|
t.Errorf("Attribute %d not changed, got %v", i+1, node.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@@ -16,7 +16,7 @@ fi
|
|||||||
echo "Tag: $TAG"
|
echo "Tag: $TAG"
|
||||||
|
|
||||||
echo "Building the thing..."
|
echo "Building the thing..."
|
||||||
go build -o BigChef.exe .
|
go build -o chef.exe .
|
||||||
|
|
||||||
echo "Creating a release..."
|
echo "Creating a release..."
|
||||||
TOKEN="$GITEA_API_KEY"
|
TOKEN="$GITEA_API_KEY"
|
||||||
@@ -43,6 +43,6 @@ echo "Release ID: $RELEASE_ID"
|
|||||||
echo "Uploading the things..."
|
echo "Uploading the things..."
|
||||||
curl -X POST \
|
curl -X POST \
|
||||||
-H "Authorization: token $TOKEN" \
|
-H "Authorization: token $TOKEN" \
|
||||||
-F "attachment=@BigChef.exe" \
|
-F "attachment=@chef.exe" \
|
||||||
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=BigChef.exe"
|
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=chef.exe"
|
||||||
rm BigChef.exe
|
rm chef.exe
|
||||||
|
Reference in New Issue
Block a user