28 Commits

Author SHA1 Message Date
34477b2c34 Make readme and rework the flags a little 2025-03-25 22:55:01 +01:00
d5c08d86f5 Code polish 2025-03-25 19:22:44 +01:00
68127fe453 Add more json tests
To bring it in line with the xml ones
2025-03-25 19:22:07 +01:00
872f2dd46d Fix another changed test for json 2025-03-25 19:16:09 +01:00
4eed05c7c2 Fix some more minor bugs and tests 2025-03-25 19:14:21 +01:00
4640281fbf Enable root modifications
Though I can not see why you would want to.....
But there's no reason you would not be able to
2025-03-25 18:57:32 +01:00
aba10267d1 Fix more tests 2025-03-25 18:47:55 +01:00
fed140254b Fix some json tests 2025-03-25 18:32:51 +01:00
db92033642 Rework rounding and building lua script
To allow user script to specify what was modified where
2025-03-25 18:28:33 +01:00
1b0b198297 Add xpath tests 2025-03-25 17:56:48 +01:00
15ae116447 Fix up some miscellaneous shit around the project regarding lua conversions 2025-03-25 17:46:21 +01:00
1bcc6735ab Improve error handling across the board 2025-03-25 17:27:20 +01:00
20bab894e3 Add path data to the selected nodes for reconstruction via set 2025-03-25 17:27:20 +01:00
396992b3d0 Implement Set and SetAll (and Get) 2025-03-25 17:27:20 +01:00
054dcbf835 Fix up the recursive descent
Again I guess?
2025-03-25 17:27:20 +01:00
88887b9a12 Fix up the recursive descent 2025-03-25 17:27:20 +01:00
533a563dc5 Fix up parser 2025-03-25 17:27:20 +01:00
7fc2956b6d Begin to implement jsonpath 2025-03-25 17:27:20 +01:00
6f9f3f5eae Begin to rework the json parsing 2025-03-25 17:27:20 +01:00
d2419b761e Implement a generic translator between lua and go tables (maps) 2025-03-25 17:27:20 +01:00
ab98800ca0 Rework regex processor to be more betterer 2025-03-25 17:27:20 +01:00
0f7ee521ac Fix up lua variable writing and reading for regex 2025-03-25 17:27:20 +01:00
fd81861a64 Fix reading lua variables 2025-03-25 17:27:20 +01:00
430234dd3b Clean up after claude 2025-03-25 17:27:20 +01:00
17bb3d4f71 Refactor everything to processors and implement json and xml processors such as they are 2025-03-25 17:27:20 +01:00
84e0a8bed6 Add xpath dependencies 2025-03-25 17:27:20 +01:00
bdcf096fdd Fix up the oopsies 2025-03-25 17:27:15 +01:00
1a90046c89 Add release script 2025-03-25 17:26:23 +01:00
14 changed files with 1883 additions and 251 deletions

9
.vscode/launch.json vendored
View File

@@ -9,8 +9,13 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${fileDirname}", "program": "${workspaceFolder}",
"args": [] "args": [
"-mode=json",
"$..name",
"v='pero'",
"test.json"
]
} }
] ]
} }

116
README.md Normal file
View 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
View File

@@ -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
View File

@@ -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=

54
main.go
View File

@@ -19,20 +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")
verboseFlag = flag.Bool("verbose", false, "Enable verbose output") xmlFlag = flag.Bool("xml", false, "Process XML files")
) )
func init() { func init() {
@@ -48,12 +40,6 @@ func main() {
fmt.Fprintf(os.Stderr, "\nOptions:\n") fmt.Fprintf(os.Stderr, "\nOptions:\n")
fmt.Fprintf(os.Stderr, " -mode string\n") fmt.Fprintf(os.Stderr, " -mode string\n")
fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n") fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n")
fmt.Fprintf(os.Stderr, " -xpath string\n")
fmt.Fprintf(os.Stderr, " XPath expression (for XML mode)\n")
fmt.Fprintf(os.Stderr, " -jsonpath string\n")
fmt.Fprintf(os.Stderr, " JSONPath expression (for JSON mode)\n")
fmt.Fprintf(os.Stderr, " -verbose\n")
fmt.Fprintf(os.Stderr, " Enable verbose output\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " Regex mode (default):\n") fmt.Fprintf(os.Stderr, " Regex mode (default):\n")
fmt.Fprintf(os.Stderr, " %s \"<value>(\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s \"<value>(\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0])
@@ -74,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
} }
@@ -83,15 +69,9 @@ func main() {
var pattern, luaExpr string var pattern, luaExpr string
var filePatterns []string var filePatterns []string
if *fileModeFlag == "regex" { pattern = args[0]
pattern = args[0] luaExpr = args[1]
luaExpr = args[1] filePatterns = args[2:]
filePatterns = args[2:]
} else {
// For XML/JSON modes, pattern comes from flags
luaExpr = args[0]
filePatterns = args[1:]
}
// Prepare the Lua expression // Prepare the Lua expression
originalLuaExpr := luaExpr originalLuaExpr := luaExpr
@@ -114,21 +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.XMLProcessor{}
logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
pattern, luaExpr, len(files))
case *jsonFlag:
proc = &processor.JSONProcessor{}
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
pattern, luaExpr, len(files))
default:
proc = &processor.RegexProcessor{} proc = &processor.RegexProcessor{}
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files", logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
pattern, luaExpr, len(files)) pattern, luaExpr, len(files))
// case "xml":
// 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{}
// pattern = *jsonpathFlag
// logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
// pattern, luaExpr, len(files))
} }
var wg sync.WaitGroup var wg sync.WaitGroup

View File

@@ -3,10 +3,10 @@ package processor
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"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"
) )
@@ -67,72 +67,65 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
return content, 0, 0, nil return content, 0, 0, nil
} }
// Initialize Lua modCount := 0
L, err := NewLuaState() for _, node := range nodes {
if err != nil { log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
}
defer L.Close()
err = p.ToLua(L, nodes) // Initialize Lua
if err != nil { L, err := NewLuaState()
return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err) if err != nil {
} return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
}
defer L.Close()
log.Println("Lua state initialized successfully.")
// Execute Lua script err = p.ToLua(L, node.Value)
if err := L.DoString(luaExpr); err != nil { if err != nil {
return content, len(nodes), 0, fmt.Errorf("error executing Lua %s: %v", luaExpr, err) return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
} }
log.Printf("Converted node value to Lua: %v", node.Value)
// Get modified value originalScript := luaExpr
result, err := p.FromLua(L) fullScript := BuildLuaScript(luaExpr)
if err != nil { log.Printf("Original script: %q, Full script: %q", originalScript, fullScript)
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
}
// Apply the modification to the JSON data // Execute Lua script
err = p.updateJSONValue(jsonData, pattern, result) log.Printf("Executing Lua script: %q", fullScript)
if err != nil { if err := L.DoString(fullScript); err != nil {
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err) return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
}
log.Println("Lua script executed successfully.")
// Get modified value
result, err := p.FromLua(L)
if err != nil {
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
}
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
err = p.updateJSONValue(jsonData, node.Path, result)
if err != nil {
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)
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 != "" { jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
// Use detected indentation for output formatting if err != nil {
jsonBytes, err = json.MarshalIndent(jsonData, "", indent) return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
} else {
// Fall back to standard 2-space indent
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
} }
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
@@ -154,12 +147,62 @@ 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)
if err != nil {
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
}
return nil return nil
} }
// ToLua converts JSON values to Lua variables // ToLua converts JSON values to Lua variables
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error { func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
table, err := ToLuaTable(L, data) table, err := ToLua(L, data)
if err != nil { if err != nil {
return err return err
} }
@@ -170,5 +213,5 @@ func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
// FromLua retrieves values from Lua // FromLua retrieves values from Lua
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) { func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
luaValue := L.GetGlobal("v") luaValue := L.GetGlobal("v")
return FromLuaTable(L, luaValue.(*lua.LTable)) return FromLua(L, luaValue)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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
} }

View File

@@ -2,7 +2,6 @@ package processor
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -52,65 +51,97 @@ func NewLuaState() (*lua.LState, error) {
return L, nil return L, nil
} }
// ToLuaTable converts a struct or map to a Lua table recursively // ToLua converts a struct or map to a Lua table recursively
func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) { func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
luaTable := L.NewTable()
switch v := data.(type) { switch v := data.(type) {
case map[string]interface{}: case map[string]interface{}:
luaTable := L.NewTable()
for key, value := range v { for key, value := range v {
luaValue, err := ToLuaTable(L, value) luaValue, err := ToLua(L, value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
luaTable.RawSetString(key, luaValue) luaTable.RawSetString(key, luaValue)
} }
case struct{}: return luaTable, nil
val := reflect.ValueOf(v) case []interface{}:
for i := 0; i < val.NumField(); i++ { luaTable := L.NewTable()
field := val.Type().Field(i) for i, value := range v {
luaValue, err := ToLuaTable(L, val.Field(i).Interface()) luaValue, err := ToLua(L, value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
luaTable.RawSetString(field.Name, luaValue) luaTable.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
} }
return luaTable, nil
case string: case string:
luaTable.RawSetString("v", lua.LString(v)) return lua.LString(v), nil
case bool: case bool:
luaTable.RawSetString("v", lua.LBool(v)) return lua.LBool(v), nil
case float64: case float64:
luaTable.RawSetString("v", lua.LNumber(v)) 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)
} }
return luaTable, nil
} }
// FromLuaTable converts a Lua table to a struct or map recursively // FromLua converts a Lua table to a struct or map recursively
func FromLuaTable(L *lua.LState, luaTable *lua.LTable) (map[string]interface{}, error) { func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
result := make(map[string]interface{}) switch v := luaValue.(type) {
// Well shit...
luaTable.ForEach(func(key lua.LValue, value lua.LValue) { // Tables in lua are both maps and arrays
switch v := value.(type) { // As arrays they are ordered and as maps, obviously, not
case *lua.LTable: // So when we parse them to a go map we fuck up the order for arrays
nestedMap, err := FromLuaTable(L, v) // We have to find a better way....
if err != nil { case *lua.LTable:
return isArray, err := IsLuaTableArray(L, v)
} if err != nil {
result[key.String()] = nestedMap return nil, err
case lua.LString:
result[key.String()] = string(v)
case lua.LBool:
result[key.String()] = bool(v)
case lua.LNumber:
result[key.String()] = float64(v)
default:
result[key.String()] = nil
} }
}) if isArray {
result := make([]interface{}, 0)
v.ForEach(func(key lua.LValue, value lua.LValue) {
converted, _ := FromLua(L, value)
result = append(result, converted)
})
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:
return string(v), nil
case lua.LBool:
return bool(v), nil
case lua.LNumber:
return float64(v), nil
default:
return nil, nil
}
}
return result, nil 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
@@ -119,7 +150,10 @@ func InitLuaHelpers(L *lua.LState) error {
-- 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
@@ -139,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)
@@ -157,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, "/") ||
@@ -176,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 {

View File

@@ -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
View 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")
}

View 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)
}
}
})
}

48
release.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
echo "Figuring out the tag..."
TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
# Get the latest tag
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
# Increment the patch version
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
TAG="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
# Create a new tag
git tag $TAG
git push origin $TAG
fi
echo "Tag: $TAG"
echo "Building the thing..."
go build -o chef.exe .
echo "Creating a release..."
TOKEN="$GITEA_API_KEY"
GITEA="https://git.site.quack-lab.dev"
REPO="dave/BigChef"
# Create a release
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "'"$TAG"'",
"name": "'"$TAG"'",
"draft": false,
"prerelease": false
}' \
$GITEA/api/v1/repos/$REPO/releases)
# Extract the release ID
echo $RELEASE_RESPONSE
RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
echo "Release ID: $RELEASE_ID"
echo "Uploading the things..."
curl -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@chef.exe" \
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=chef.exe"
rm chef.exe