Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
73d93367a0 | |||
64f690f6b4 | |||
34477b2c34 | |||
d5c08d86f5 | |||
68127fe453 | |||
872f2dd46d |
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=
|
|
||||||
|
56
main.go
56
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() {
|
||||||
@@ -42,22 +35,39 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// TODO: Implement some sort of git integration
|
||||||
|
// Maybe use go-git
|
||||||
|
// Specify a -git flag
|
||||||
|
// If we are operating with git then:
|
||||||
|
// Inmitialize a repo if one doesn't exist (try to open right?)
|
||||||
|
// For each file matched by glob first figure out if it's already tracked
|
||||||
|
// If not tracked then track it and commit (either it alone or maybe multiple together somehow)
|
||||||
|
// Then reset the file (to undo previous modifications)
|
||||||
|
// THEN change the file
|
||||||
|
// In addition add a -undo flag that will ONLY reset the files without changing them
|
||||||
|
// Only for the ones matched by glob
|
||||||
|
// ^ important because binary files would fuck us up
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -json\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Process JSON files\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -xml\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Process XML files\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, "\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])
|
||||||
fmt.Fprintf(os.Stderr, " XML mode:\n")
|
fmt.Fprintf(os.Stderr, " XML mode:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s -mode=xml -xpath=\"//value\" \"*1.5\" data.xml\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -xml \"//value\" \"*1.5\" data.xml\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s -mode=json -jsonpath=\"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -json \"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n")
|
fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n")
|
||||||
fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n")
|
fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n")
|
||||||
fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n")
|
fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n")
|
||||||
fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n")
|
fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " For XML and JSON, the captured values are exposed as 'v', which can be of any type we capture (string, number, table).\n")
|
||||||
fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
|
fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
|
||||||
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
|
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
|
||||||
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
|
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
|
||||||
@@ -67,7 +77,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 +111,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
|
||||||
@@ -125,7 +134,8 @@ func main() {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
logger.Printf("Processing file: %s", file)
|
logger.Printf("Processing file: %s", file)
|
||||||
|
|
||||||
modCount, matchCount, err := proc.Process(file, pattern, luaExpr)
|
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
|
||||||
|
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
||||||
stats.FailedFiles++
|
stats.FailedFiles++
|
||||||
|
@@ -5,8 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"modify/processor/jsonpath"
|
"modify/processor/jsonpath"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
@@ -14,39 +12,6 @@ import (
|
|||||||
// JSONProcessor implements the Processor interface for JSON documents
|
// JSONProcessor implements the Processor interface for JSON documents
|
||||||
type JSONProcessor struct{}
|
type JSONProcessor struct{}
|
||||||
|
|
||||||
// Process implements the Processor interface for JSONProcessor
|
|
||||||
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
|
||||||
// Read file content
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := filepath.Join(cwd, filename)
|
|
||||||
content, err := os.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent := string(content)
|
|
||||||
|
|
||||||
// Process the content
|
|
||||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we made modifications, save the file
|
|
||||||
if modCount > 0 {
|
|
||||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modCount, matchCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessContent implements the Processor interface for JSONProcessor
|
// ProcessContent implements the Processor interface for JSONProcessor
|
||||||
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||||
// Parse JSON document
|
// Parse JSON document
|
||||||
|
@@ -1096,3 +1096,676 @@ func TestJSONProcessor_RootNodeModification(t *testing.T) {
|
|||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_DateManipulation tests manipulating date strings in a JSON document
|
||||||
|
func TestJSONProcessor_DateManipulation(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "Conference",
|
||||||
|
"date": "2023-06-15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Workshop",
|
||||||
|
"date": "2023-06-20"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "Conference",
|
||||||
|
"date": "2023-07-15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Workshop",
|
||||||
|
"date": "2023-07-20"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.events[*].date", `
|
||||||
|
local year, month, day = string.match(v, "(%d%d%d%d)-(%d%d)-(%d%d)")
|
||||||
|
-- Postpone events by 1 month
|
||||||
|
month = tonumber(month) + 1
|
||||||
|
if month > 12 then
|
||||||
|
month = 1
|
||||||
|
year = tonumber(year) + 1
|
||||||
|
end
|
||||||
|
v = string.format("%04d-%02d-%s", tonumber(year), month, day)
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 2 {
|
||||||
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 2 {
|
||||||
|
t.Errorf("Expected 2 modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse results as JSON objects for deep comparison rather than string comparison
|
||||||
|
var resultObj map[string]interface{}
|
||||||
|
var expectedObj map[string]interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the events arrays
|
||||||
|
resultEvents, ok := resultObj["events"].([]interface{})
|
||||||
|
if !ok || len(resultEvents) != 2 {
|
||||||
|
t.Fatalf("Expected events array with 2 items in result")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedEvents, ok := expectedObj["events"].([]interface{})
|
||||||
|
if !ok || len(expectedEvents) != 2 {
|
||||||
|
t.Fatalf("Expected events array with 2 items in expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each event's date value
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
resultEvent, ok := resultEvents[i].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected event %d to be an object", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedEvent, ok := expectedEvents[i].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected expected event %d to be an object", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDate, ok := resultEvent["date"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected date in result event %d to be a string", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate, ok := expectedEvent["date"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected date in expected event %d to be a string", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultDate != expectedDate {
|
||||||
|
t.Errorf("Event %d: expected date %s, got %s", i, expectedDate, resultDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_MathFunctions tests using math functions in JSON processing
|
||||||
|
func TestJSONProcessor_MathFunctions(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"measurements": [
|
||||||
|
3.14159,
|
||||||
|
2.71828,
|
||||||
|
1.41421
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"measurements": [
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.measurements[*]", "v = round(v)")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 3 {
|
||||||
|
t.Errorf("Expected 3 matches, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 3 {
|
||||||
|
t.Errorf("Expected 3 modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace for comparison
|
||||||
|
normalizedResult := normalizeWhitespace(result)
|
||||||
|
normalizedExpected := normalizeWhitespace(expected)
|
||||||
|
|
||||||
|
if normalizedResult != normalizedExpected {
|
||||||
|
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_Error_InvalidJSON tests error handling for invalid JSON
|
||||||
|
func TestJSONProcessor_Error_InvalidJSON(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"unclosed": "value"
|
||||||
|
`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
_, _, _, err := p.ProcessContent(content, "$.unclosed", "v='modified'")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error for invalid JSON, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_Error_InvalidJSONPath tests error handling for invalid JSONPath
|
||||||
|
func TestJSONProcessor_Error_InvalidJSONPath(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"element": "value"
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
_, _, _, err := p.ProcessContent(content, "[invalid path]", "v='modified'")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error for invalid JSONPath, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_Error_InvalidLua tests error handling for invalid Lua
|
||||||
|
func TestJSONProcessor_Error_InvalidLua(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"element": 123
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
_, _, _, err := p.ProcessContent(content, "$.element", "v = invalid_function()")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error for invalid Lua, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_Process_SpecialCharacters tests handling of special characters in JSON
|
||||||
|
func TestJSONProcessor_Process_SpecialCharacters(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"data": [
|
||||||
|
"This & that",
|
||||||
|
"a < b",
|
||||||
|
"c > d",
|
||||||
|
"Quote: \"Hello\""
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.data[*]", "v = string.upper(v)")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 4 {
|
||||||
|
t.Errorf("Expected 4 matches, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 4 {
|
||||||
|
t.Errorf("Expected 4 modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the result to verify the content
|
||||||
|
var resultObj map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := resultObj["data"].([]interface{})
|
||||||
|
if !ok || len(data) != 4 {
|
||||||
|
t.Fatalf("Expected data array with 4 items")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedValues := []string{
|
||||||
|
"THIS & THAT",
|
||||||
|
"A < B",
|
||||||
|
"C > D",
|
||||||
|
"QUOTE: \"HELLO\"",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, val := range data {
|
||||||
|
strVal, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected item %d to be a string", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strVal != expectedValues[i] {
|
||||||
|
t.Errorf("Item %d: expected %q, got %q", i, expectedValues[i], strVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_AggregateCalculation tests calculating aggregated values from multiple fields
|
||||||
|
func TestJSONProcessor_AggregateCalculation(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Apple",
|
||||||
|
"price": 1.99,
|
||||||
|
"quantity": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Carrot",
|
||||||
|
"price": 0.99,
|
||||||
|
"quantity": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Apple",
|
||||||
|
"price": 1.99,
|
||||||
|
"quantity": 10,
|
||||||
|
"total": 19.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Carrot",
|
||||||
|
"price": 0.99,
|
||||||
|
"quantity": 5,
|
||||||
|
"total": 4.95
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.items[*]", `
|
||||||
|
-- Calculate total from price and quantity
|
||||||
|
local price = v.price
|
||||||
|
local quantity = v.quantity
|
||||||
|
|
||||||
|
-- Add new total field
|
||||||
|
v.total = price * quantity
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 2 {
|
||||||
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 2 {
|
||||||
|
t.Errorf("Expected 2 modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace for comparison
|
||||||
|
normalizedResult := normalizeWhitespace(result)
|
||||||
|
normalizedExpected := normalizeWhitespace(expected)
|
||||||
|
|
||||||
|
if normalizedResult != normalizedExpected {
|
||||||
|
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_DataAnonymization tests anonymizing sensitive data
|
||||||
|
func TestJSONProcessor_DataAnonymization(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"contacts": [
|
||||||
|
{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john.doe@example.com",
|
||||||
|
"phone": "123-456-7890"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"email": "jane.smith@example.com",
|
||||||
|
"phone": "456-789-0123"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
|
||||||
|
// First pass: anonymize email addresses
|
||||||
|
result, modCount1, matchCount1, err := p.ProcessContent(content, "$.contacts[*].email", `
|
||||||
|
-- Anonymize email
|
||||||
|
v = string.gsub(v, "@.+", "@anon.com")
|
||||||
|
local username = string.match(v, "(.+)@")
|
||||||
|
v = string.gsub(username, "%.", "") .. "@anon.com"
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing email content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: anonymize phone numbers
|
||||||
|
result, modCount2, matchCount2, err := p.ProcessContent(result, "$.contacts[*].phone", `
|
||||||
|
-- Mask phone numbers
|
||||||
|
v = string.gsub(v, "%d%d%d%-%d%d%d%-%d%d%d%d", function(match)
|
||||||
|
return string.sub(match, 1, 3) .. "-XXX-XXXX"
|
||||||
|
end)
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing phone content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total counts from both operations
|
||||||
|
matchCount := matchCount1 + matchCount2
|
||||||
|
modCount := modCount1 + modCount2
|
||||||
|
|
||||||
|
if matchCount != 4 {
|
||||||
|
t.Errorf("Expected 4 total matches, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 4 {
|
||||||
|
t.Errorf("Expected 4 total modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the resulting JSON for validating content
|
||||||
|
var resultObj map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts, ok := resultObj["contacts"].([]interface{})
|
||||||
|
if !ok || len(contacts) != 2 {
|
||||||
|
t.Fatalf("Expected contacts array with 2 items")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate first contact
|
||||||
|
contact1, ok := contacts[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected first contact to be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if email1, ok := contact1["email"].(string); !ok || email1 != "johndoe@anon.com" {
|
||||||
|
t.Errorf("First contact email should be johndoe@anon.com, got %v", contact1["email"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if phone1, ok := contact1["phone"].(string); !ok || phone1 != "123-XXX-XXXX" {
|
||||||
|
t.Errorf("First contact phone should be 123-XXX-XXXX, got %v", contact1["phone"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate second contact
|
||||||
|
contact2, ok := contacts[1].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected second contact to be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if email2, ok := contact2["email"].(string); !ok || email2 != "janesmith@anon.com" {
|
||||||
|
t.Errorf("Second contact email should be janesmith@anon.com, got %v", contact2["email"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if phone2, ok := contact2["phone"].(string); !ok || phone2 != "456-XXX-XXXX" {
|
||||||
|
t.Errorf("Second contact phone should be 456-XXX-XXXX, got %v", contact2["phone"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_ChainedOperations tests sequential operations on the same data
|
||||||
|
func TestJSONProcessor_ChainedOperations(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"product": {
|
||||||
|
"name": "Widget",
|
||||||
|
"price": 100
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"product": {
|
||||||
|
"name": "Widget",
|
||||||
|
"price": 103.5
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.product.price", `
|
||||||
|
-- When v is a numeric value, we can perform math operations directly
|
||||||
|
local price = v
|
||||||
|
-- Add 15% tax
|
||||||
|
price = price * 1.15
|
||||||
|
-- Apply 10% discount
|
||||||
|
price = price * 0.9
|
||||||
|
-- Round to 2 decimal places
|
||||||
|
price = math.floor(price * 100 + 0.5) / 100
|
||||||
|
v = price
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 1 {
|
||||||
|
t.Errorf("Expected 1 match, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 1 {
|
||||||
|
t.Errorf("Expected 1 modification, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace for comparison
|
||||||
|
normalizedResult := normalizeWhitespace(result)
|
||||||
|
normalizedExpected := normalizeWhitespace(expected)
|
||||||
|
|
||||||
|
if normalizedResult != normalizedExpected {
|
||||||
|
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONProcessor_ComplexDataTransformation tests advanced JSON transformation
|
||||||
|
func TestJSONProcessor_ComplexDataTransformation(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"store": {
|
||||||
|
"name": "My Store",
|
||||||
|
"inventory": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Laptop",
|
||||||
|
"category": "electronics",
|
||||||
|
"price": 999.99,
|
||||||
|
"stock": 15,
|
||||||
|
"features": ["16GB RAM", "512GB SSD", "15-inch display"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Smartphone",
|
||||||
|
"category": "electronics",
|
||||||
|
"price": 499.99,
|
||||||
|
"stock": 25,
|
||||||
|
"features": ["6GB RAM", "128GB storage", "5G"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "T-Shirt",
|
||||||
|
"category": "clothing",
|
||||||
|
"price": 19.99,
|
||||||
|
"stock": 100,
|
||||||
|
"features": ["100% cotton", "M, L, XL sizes", "Red color"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Headphones",
|
||||||
|
"category": "electronics",
|
||||||
|
"price": 149.99,
|
||||||
|
"stock": 8,
|
||||||
|
"features": ["Noise cancelling", "Bluetooth", "20hr battery"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := `{
|
||||||
|
"store": {
|
||||||
|
"name": "My Store",
|
||||||
|
"inventory_summary": {
|
||||||
|
"electronics": {
|
||||||
|
"count": 3,
|
||||||
|
"total_value": 30924.77,
|
||||||
|
"low_stock_items": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Headphones",
|
||||||
|
"stock": 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clothing": {
|
||||||
|
"count": 1,
|
||||||
|
"total_value": 1999.00,
|
||||||
|
"low_stock_items": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transformed_items": [
|
||||||
|
{
|
||||||
|
"name": "Laptop",
|
||||||
|
"price_with_tax": 1199.99,
|
||||||
|
"in_stock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Smartphone",
|
||||||
|
"price_with_tax": 599.99,
|
||||||
|
"in_stock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "T-Shirt",
|
||||||
|
"price_with_tax": 23.99,
|
||||||
|
"in_stock": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Headphones",
|
||||||
|
"price_with_tax": 179.99,
|
||||||
|
"in_stock": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := &JSONProcessor{}
|
||||||
|
|
||||||
|
// First, create a complex transformation that:
|
||||||
|
// 1. Summarizes inventory by category (count, total value, low stock alerts)
|
||||||
|
// 2. Creates a simplified view of items with tax added
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "$", `
|
||||||
|
-- Get store data
|
||||||
|
local store = v.store
|
||||||
|
local inventory = store.inventory
|
||||||
|
|
||||||
|
-- Remove the original inventory array, we'll replace it with our summaries
|
||||||
|
store.inventory = nil
|
||||||
|
|
||||||
|
-- Create summary by category
|
||||||
|
local summary = {}
|
||||||
|
local transformed = {}
|
||||||
|
|
||||||
|
-- Group and analyze items by category
|
||||||
|
for _, item in ipairs(inventory) do
|
||||||
|
-- Prepare category data if not exists
|
||||||
|
local category = item.category
|
||||||
|
if not summary[category] then
|
||||||
|
summary[category] = {
|
||||||
|
count = 0,
|
||||||
|
total_value = 0,
|
||||||
|
low_stock_items = {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update category counts
|
||||||
|
summary[category].count = summary[category].count + 1
|
||||||
|
|
||||||
|
-- Calculate total value (price * stock) and add to category
|
||||||
|
local item_value = item.price * item.stock
|
||||||
|
summary[category].total_value = summary[category].total_value + item_value
|
||||||
|
|
||||||
|
-- Check for low stock (less than 10)
|
||||||
|
if item.stock < 10 then
|
||||||
|
table.insert(summary[category].low_stock_items, {
|
||||||
|
id = item.id,
|
||||||
|
name = item.name,
|
||||||
|
stock = item.stock
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create transformed view of the item with added tax
|
||||||
|
table.insert(transformed, {
|
||||||
|
name = item.name,
|
||||||
|
price_with_tax = math.floor((item.price * 1.2) * 100 + 0.5) / 100, -- 20% tax, rounded to 2 decimals
|
||||||
|
in_stock = item.stock > 0
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format the total_value with two decimal places
|
||||||
|
for category, data in pairs(summary) do
|
||||||
|
data.total_value = math.floor(data.total_value * 100 + 0.5) / 100
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add our new data structures to the store
|
||||||
|
store.inventory_summary = summary
|
||||||
|
store.transformed_items = transformed
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 1 {
|
||||||
|
t.Errorf("Expected 1 match, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 1 {
|
||||||
|
t.Errorf("Expected 1 modification, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse both results as JSON objects for deep comparison
|
||||||
|
var resultObj map[string]interface{}
|
||||||
|
var expectedObj map[string]interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
||||||
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the structure and key counts
|
||||||
|
resultStore, ok := resultObj["store"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected 'store' object in result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that inventory is gone and replaced with our new structures
|
||||||
|
if resultStore["inventory"] != nil {
|
||||||
|
t.Errorf("Expected 'inventory' to be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultStore["inventory_summary"] == nil {
|
||||||
|
t.Errorf("Expected 'inventory_summary' to be added")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultStore["transformed_items"] == nil {
|
||||||
|
t.Errorf("Expected 'transformed_items' to be added")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the transformed_items array has the correct length
|
||||||
|
transformedItems, ok := resultStore["transformed_items"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected 'transformed_items' to be an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(transformedItems) != 4 {
|
||||||
|
t.Errorf("Expected 'transformed_items' to have 4 items, got %d", len(transformedItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the summary has entries for both electronics and clothing
|
||||||
|
summary, ok := resultStore["inventory_summary"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected 'inventory_summary' to be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary["electronics"] == nil {
|
||||||
|
t.Errorf("Expected 'electronics' category in summary")
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary["clothing"] == nil {
|
||||||
|
t.Errorf("Expected 'clothing' category in summary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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,6 +2,8 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
@@ -10,7 +12,8 @@ import (
|
|||||||
// Processor defines the interface for all file processors
|
// Processor defines the interface for all file processors
|
||||||
type Processor interface {
|
type Processor interface {
|
||||||
// Process handles processing a file with the given pattern and Lua expression
|
// Process handles processing a file with the given pattern and Lua expression
|
||||||
Process(filename string, pattern string, luaExpr string) (int, int, error)
|
// Now implemented as a base function in processor.go
|
||||||
|
// Process(filename string, pattern string, luaExpr string) (int, int, error)
|
||||||
|
|
||||||
// ProcessContent handles processing a string content directly with the given pattern and Lua expression
|
// ProcessContent handles processing a string content directly with the given pattern and Lua expression
|
||||||
// Returns the modified content, modification count, match count, and any error
|
// Returns the modified content, modification count, match count, and any error
|
||||||
@@ -51,6 +54,38 @@ func NewLuaState() (*lua.LState, error) {
|
|||||||
return L, nil
|
return L, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
|
// Read file content
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(cwd, filename)
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent := string(content)
|
||||||
|
|
||||||
|
// Process the content
|
||||||
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made modifications, save the file
|
||||||
|
if modCount > 0 {
|
||||||
|
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modCount, matchCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ToLua converts a struct or map to a Lua table recursively
|
// ToLua converts a struct or map to a Lua table recursively
|
||||||
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
|
@@ -2,8 +2,6 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,34 +12,6 @@ import (
|
|||||||
// RegexProcessor implements the Processor interface using regex patterns
|
// RegexProcessor implements the Processor interface using regex patterns
|
||||||
type RegexProcessor struct{}
|
type RegexProcessor struct{}
|
||||||
|
|
||||||
// Process implements the Processor interface for RegexProcessor
|
|
||||||
func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
|
||||||
// Read file content
|
|
||||||
fullPath := filepath.Join(".", filename)
|
|
||||||
content, err := os.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent := string(content)
|
|
||||||
|
|
||||||
// Process the content
|
|
||||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we made modifications, save the file
|
|
||||||
if modCount > 0 {
|
|
||||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modCount, matchCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
||||||
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
|
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||||
captures, ok := data.([]string)
|
captures, ok := data.([]string)
|
||||||
|
@@ -2,8 +2,6 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
"github.com/antchfx/xmlquery"
|
||||||
@@ -13,34 +11,6 @@ import (
|
|||||||
// XMLProcessor implements the Processor interface for XML documents
|
// XMLProcessor implements the Processor interface for XML documents
|
||||||
type XMLProcessor struct{}
|
type XMLProcessor struct{}
|
||||||
|
|
||||||
// Process implements the Processor interface for XMLProcessor
|
|
||||||
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
|
||||||
// Read file content
|
|
||||||
fullPath := filepath.Join(".", filename)
|
|
||||||
content, err := os.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent := string(content)
|
|
||||||
|
|
||||||
// Process the content
|
|
||||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we made modifications, save the file
|
|
||||||
if modCount > 0 {
|
|
||||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modCount, matchCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessContent implements the Processor interface for XMLProcessor
|
// ProcessContent implements the Processor interface for XMLProcessor
|
||||||
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||||
// Parse XML document
|
// Parse XML document
|
||||||
|
@@ -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