208 lines
6.5 KiB
Go
208 lines
6.5 KiB
Go
package processor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// JSONProcessor implements the Processor interface for JSON documents
|
|
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
|
|
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
|
// Parse JSON document
|
|
var jsonData interface{}
|
|
err := json.Unmarshal([]byte(content), &jsonData)
|
|
if err != nil {
|
|
return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err)
|
|
}
|
|
|
|
// Find nodes matching the JSONPath pattern
|
|
paths, values, err := p.findJSONPaths(jsonData, pattern)
|
|
if err != nil {
|
|
return content, 0, 0, fmt.Errorf("error executing JSONPath: %v", err)
|
|
}
|
|
|
|
matchCount := len(paths)
|
|
if matchCount == 0 {
|
|
return content, 0, 0, nil
|
|
}
|
|
|
|
// Initialize Lua
|
|
L, err := NewLuaState()
|
|
if err != nil {
|
|
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
|
}
|
|
defer L.Close()
|
|
|
|
// Apply modifications to each node
|
|
modCount := 0
|
|
for i, value := range values {
|
|
// Convert to Lua variables
|
|
err = p.ToLua(L, value)
|
|
if err != nil {
|
|
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
|
|
}
|
|
|
|
// Execute Lua script
|
|
if err := L.DoString(luaExpr); err != nil {
|
|
return content, modCount, matchCount, fmt.Errorf("error executing Lua %s: %v", luaExpr, err)
|
|
}
|
|
|
|
// Get modified value
|
|
result, err := p.FromLua(L)
|
|
if err != nil {
|
|
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
|
}
|
|
|
|
// Apply the modification to the JSON data
|
|
err = p.updateJSONValue(jsonData, paths[i], result)
|
|
if err != nil {
|
|
return content, modCount, matchCount, fmt.Errorf("error updating JSON: %v", err)
|
|
}
|
|
|
|
// Increment mod count if we haven't already counted object properties
|
|
modCount++
|
|
}
|
|
|
|
// Convert the modified JSON back to a string with same formatting
|
|
var jsonBytes []byte
|
|
if indent, err := detectJsonIndentation(content); err == nil && indent != "" {
|
|
// Use detected indentation for output formatting
|
|
jsonBytes, err = json.MarshalIndent(jsonData, "", indent)
|
|
} else {
|
|
// Fall back to standard 2-space indent
|
|
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
|
}
|
|
|
|
if err != nil {
|
|
return content, modCount, matchCount, fmt.Errorf("error serializing JSON: %v", err)
|
|
}
|
|
|
|
return string(jsonBytes), modCount, matchCount, 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")
|
|
}
|
|
|
|
func (p *JSONProcessor) findJSONPaths(jsonData interface{}, pattern string) ([]string, []interface{}, error) {
|
|
// / $ the root object/element
|
|
// // .. recursive descent. JSONPath borrows this syntax from E4X.
|
|
// * * wildcard. All objects/elements regardless their names.
|
|
// [] [] subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator.
|
|
|
|
// patternPaths := strings.Split(pattern, ".")
|
|
// current := jsonData
|
|
// for _, path := range patternPaths {
|
|
// switch path {
|
|
// case "$":
|
|
// current = jsonData
|
|
// case "@":
|
|
// current = jsonData
|
|
// case "*":
|
|
// current = jsonData
|
|
// case "..":
|
|
// }
|
|
// }
|
|
// paths, values, err := p.findJSONPaths(jsonData, pattern)
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// / Selects from the root node
|
|
// // Selects nodes in the document from the current node that match the selection no matter where they are
|
|
// . Selects the current node
|
|
// @ Selects attributes
|
|
|
|
// /bookstore/* Selects all the child element nodes of the bookstore element
|
|
// //* Selects all elements in the document
|
|
|
|
// /bookstore/book[1] Selects the first book element that is the child of the bookstore element.
|
|
// /bookstore/book[last()] Selects the last book element that is the child of the bookstore element
|
|
// /bookstore/book[last()-1] Selects the last but one book element that is the child of the bookstore element
|
|
// /bookstore/book[position()<3] Selects the first two book elements that are children of the bookstore element
|
|
// //title[@lang] Selects all the title elements that have an attribute named lang
|
|
// //title[@lang='en'] Selects all the title elements that have a "lang" attribute with a value of "en"
|
|
// /bookstore/book[price>35.00] Selects all the book elements of the bookstore element that have a price element with a value greater than 35.00
|
|
// /bookstore/book[price>35.00]/title Selects all the title elements of the book elements of the bookstore element that have a price element with a value greater than 35.00
|
|
|
|
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
|
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
|
return nil
|
|
}
|
|
|
|
// ToLua converts JSON values to Lua variables
|
|
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
|
table, err := ToLuaTable(L, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
L.SetGlobal("v", table)
|
|
return nil
|
|
}
|
|
|
|
// FromLua retrieves values from Lua
|
|
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
|
luaValue := L.GetGlobal("v")
|
|
return FromLuaTable(L, luaValue.(*lua.LTable))
|
|
}
|