Files
BigChef/processor/json.go

610 lines
16 KiB
Go

package processor
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
lua "github.com/yuin/gopher-lua"
)
// JSONProcessor implements the Processor interface using JSONPath
type JSONProcessor struct {
Logger Logger
}
// NewJSONProcessor creates a new JSONProcessor
func NewJSONProcessor(logger Logger) *JSONProcessor {
return &JSONProcessor{
Logger: logger,
}
}
// Process implements the Processor interface for JSONProcessor
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
// Use pattern as JSONPath expression
jsonPathExpr := pattern
// 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)
if p.Logger != nil {
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
}
// Process the content
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, jsonPathExpr, luaExpr, originalExpr)
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)
}
if p.Logger != nil {
p.Logger.Printf("Made %d JSON value modifications to %s and saved (%d bytes)",
modCount, fullPath, len(modifiedContent))
}
} else if p.Logger != nil {
p.Logger.Printf("No modifications made to %s", fullPath)
}
return modCount, matchCount, nil
}
// ToLua implements the Processor interface for JSONProcessor
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
// For JSON, convert different types to appropriate Lua types
return nil
}
// FromLua implements the Processor interface for JSONProcessor
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Extract changes from Lua environment
return nil, nil
}
// ProcessContent implements the Processor interface for JSONProcessor
// It processes JSON content directly without file I/O
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) {
// Parse JSON
var jsonDoc interface{}
err := json.Unmarshal([]byte(content), &jsonDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error parsing JSON: %v", err)
}
// Log the JSONPath expression we're using
if p.Logger != nil {
p.Logger.Printf("JSON mode selected with JSONPath expression: %s", pattern)
}
// Initialize Lua state
L := lua.NewState()
defer L.Close()
// Setup Lua helper functions
if err := InitLuaHelpers(L); err != nil {
return "", 0, 0, err
}
// Setup JSON helpers
p.SetupJSONHelpers(L)
// Find matching nodes with simple JSONPath implementation
matchingPaths, err := p.findNodePaths(jsonDoc, pattern)
if err != nil {
return "", 0, 0, fmt.Errorf("error finding JSON nodes: %v", err)
}
if len(matchingPaths) == 0 {
if p.Logger != nil {
p.Logger.Printf("No JSON nodes matched JSONPath expression: %s", pattern)
}
return content, 0, 0, nil
}
if p.Logger != nil {
p.Logger.Printf("Found %d JSON nodes matching the path", len(matchingPaths))
}
// Process each node
matchCount := len(matchingPaths)
modificationCount := 0
modifications := []ModificationRecord{}
// Clone the document for modification
var modifiedDoc interface{}
modifiedBytes, err := json.Marshal(jsonDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
}
err = json.Unmarshal(modifiedBytes, &modifiedDoc)
if err != nil {
return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err)
}
// For each matching path, extract value, apply Lua script, and update
for i, path := range matchingPaths {
// Extract the original value
originalValue, err := p.getValueAtPath(jsonDoc, path)
if err != nil || originalValue == nil {
if p.Logger != nil {
p.Logger.Printf("Error getting value at path %v: %v", path, err)
}
continue
}
if p.Logger != nil {
p.Logger.Printf("Processing node #%d at path %v with value: %v", i+1, path, originalValue)
}
// Process based on the value type
switch val := originalValue.(type) {
case float64:
// Set up Lua environment for numeric value
L.SetGlobal("v1", lua.LNumber(val))
L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", val)))
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue
}
// Extract modified value
modVal := L.GetGlobal("v1")
if v, ok := modVal.(lua.LNumber); ok {
newValue := float64(v)
// Update the value in the document only if it changed
if newValue != val {
err := p.setValueAtPath(modifiedDoc, path, newValue)
if err != nil {
if p.Logger != nil {
p.Logger.Printf("Error updating value at path %v: %v", path, err)
}
continue
}
modificationCount++
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: fmt.Sprintf("%v", val),
NewValue: fmt.Sprintf("%v", newValue),
Operation: originalExpr,
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified numeric node #%d: %v -> %v", i+1, val, newValue)
}
}
}
case string:
// Set up Lua environment for string value
L.SetGlobal("s1", lua.LString(val))
// Try to convert to number if possible
if floatVal, err := strconv.ParseFloat(val, 64); err == nil {
L.SetGlobal("v1", lua.LNumber(floatVal))
} else {
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric
}
// Execute Lua script
if err := L.DoString(luaExpr); err != nil {
if p.Logger != nil {
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
}
continue
}
// Check for modifications in string (s1) or numeric (v1) values
var newValue interface{}
modified := false
// Check if s1 was modified
sVal := L.GetGlobal("s1")
if s, ok := sVal.(lua.LString); ok && string(s) != val {
newValue = string(s)
modified = true
} else {
// Check if v1 was modified to a number
vVal := L.GetGlobal("v1")
if v, ok := vVal.(lua.LNumber); ok {
numStr := strconv.FormatFloat(float64(v), 'f', -1, 64)
if numStr != val {
newValue = numStr
modified = true
}
}
}
// Apply the modification if anything changed
if modified {
err := p.setValueAtPath(modifiedDoc, path, newValue)
if err != nil {
if p.Logger != nil {
p.Logger.Printf("Error updating value at path %v: %v", path, err)
}
continue
}
modificationCount++
modifications = append(modifications, ModificationRecord{
File: "",
OldValue: val,
NewValue: fmt.Sprintf("%v", newValue),
Operation: originalExpr,
Context: fmt.Sprintf("(JSONPath: %s)", pattern),
})
if p.Logger != nil {
p.Logger.Printf("Modified string node #%d: '%s' -> '%s'",
i+1, LimitString(val, 30), LimitString(fmt.Sprintf("%v", newValue), 30))
}
}
}
}
// Marshal the modified document back to JSON with indentation
if modificationCount > 0 {
modifiedJSON, err := json.MarshalIndent(modifiedDoc, "", " ")
if err != nil {
return "", 0, 0, fmt.Errorf("error marshaling modified JSON: %v", err)
}
if p.Logger != nil {
p.Logger.Printf("Made %d JSON node modifications", modificationCount)
}
return string(modifiedJSON), modificationCount, matchCount, nil
}
// If no modifications were made, return the original content
return content, 0, matchCount, nil
}
// findNodePaths implements a simplified JSONPath for finding paths to nodes
func (p *JSONProcessor) findNodePaths(doc interface{}, path string) ([][]interface{}, error) {
// Validate the path has proper syntax
if strings.Contains(path, "[[") || strings.Contains(path, "]]") {
return nil, fmt.Errorf("invalid JSONPath syntax: %s", path)
}
// Handle root element special case
if path == "$" {
return [][]interface{}{{doc}}, nil
}
// Split path into segments
segments := strings.Split(strings.TrimPrefix(path, "$."), ".")
// Start with the root
current := [][]interface{}{{doc}}
// Process each segment
for _, segment := range segments {
var next [][]interface{}
// Handle array notation [*]
if segment == "[*]" || strings.HasSuffix(segment, "[*]") {
baseName := strings.TrimSuffix(segment, "[*]")
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
switch v := item.(type) {
case map[string]interface{}:
if baseName == "" {
// [*] means all elements at this level
for _, val := range v {
if arr, ok := val.([]interface{}); ok {
for i, elem := range arr {
newPath := make([]interface{}, len(path)+2)
copy(newPath, path)
newPath[len(path)] = i // Array index
newPath[len(path)+1] = elem
next = append(next, newPath)
}
}
}
} else if arr, ok := v[baseName].([]interface{}); ok {
for i, elem := range arr {
newPath := make([]interface{}, len(path)+3)
copy(newPath, path)
newPath[len(path)] = baseName
newPath[len(path)+1] = i // Array index
newPath[len(path)+2] = elem
next = append(next, newPath)
}
}
case []interface{}:
for i, elem := range v {
newPath := make([]interface{}, len(path)+1)
copy(newPath, path)
newPath[len(path)-1] = i // Replace last elem with index
newPath[len(path)] = elem
next = append(next, newPath)
}
}
}
current = next
continue
}
// Handle specific array indices
if strings.Contains(segment, "[") && strings.Contains(segment, "]") {
// Validate proper array syntax
if !regexp.MustCompile(`\[\d+\]$`).MatchString(segment) {
return nil, fmt.Errorf("invalid array index in JSONPath: %s", segment)
}
// Extract base name and index
baseName := segment[:strings.Index(segment, "[")]
idxStr := segment[strings.Index(segment, "[")+1 : strings.Index(segment, "]")]
idx, err := strconv.Atoi(idxStr)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", idxStr)
}
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
if obj, ok := item.(map[string]interface{}); ok {
if arr, ok := obj[baseName].([]interface{}); ok && idx < len(arr) {
newPath := make([]interface{}, len(path)+3)
copy(newPath, path)
newPath[len(path)] = baseName
newPath[len(path)+1] = idx
newPath[len(path)+2] = arr[idx]
next = append(next, newPath)
}
}
}
current = next
continue
}
// Handle regular object properties
for _, path := range current {
item := path[len(path)-1] // Get the last item in the path
if obj, ok := item.(map[string]interface{}); ok {
if val, exists := obj[segment]; exists {
newPath := make([]interface{}, len(path)+2)
copy(newPath, path)
newPath[len(path)] = segment
newPath[len(path)+1] = val
next = append(next, newPath)
}
}
}
current = next
}
return current, nil
}
// getValueAtPath extracts a value from a JSON document at the specified path
func (p *JSONProcessor) getValueAtPath(doc interface{}, path []interface{}) (interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("empty path")
}
// The last element in the path is the value itself
return path[len(path)-1], nil
}
// setValueAtPath updates a value in a JSON document at the specified path
func (p *JSONProcessor) setValueAtPath(doc interface{}, path []interface{}, newValue interface{}) error {
if len(path) < 2 {
return fmt.Errorf("path too short to update value")
}
// The path structure alternates: object/key/object/key/.../finalObject/finalKey/value
// We need to navigate to the object containing our key
// We'll get the parent object and the key to modify
// Find the parent object (second to last object) and the key (last object's property name)
// For the path structure, the parent is at index len-3 and key at len-2
if len(path) < 3 {
// Simple case: directly update the root object
rootObj, ok := doc.(map[string]interface{})
if !ok {
return fmt.Errorf("root is not an object, cannot update")
}
// Key should be a string
key, ok := path[len(path)-2].(string)
if !ok {
return fmt.Errorf("key is not a string: %v", path[len(path)-2])
}
rootObj[key] = newValue
return nil
}
// More complex case: we need to navigate to the parent object
parentIdx := len(path) - 3
keyIdx := len(path) - 2
// The actual key we need to modify
key, isString := path[keyIdx].(string)
keyInt, isInt := path[keyIdx].(int)
if !isString && !isInt {
return fmt.Errorf("key must be string or int, got %T", path[keyIdx])
}
// Get the parent object that contains the key
parent := path[parentIdx]
// If parent is a map, use string key
if parentMap, ok := parent.(map[string]interface{}); ok && isString {
parentMap[key] = newValue
return nil
}
// If parent is an array, use int key
if parentArray, ok := parent.([]interface{}); ok && isInt {
if keyInt < 0 || keyInt >= len(parentArray) {
return fmt.Errorf("array index %d out of bounds [0,%d)", keyInt, len(parentArray))
}
parentArray[keyInt] = newValue
return nil
}
return fmt.Errorf("cannot update value: parent is %T and key is %T", parent, path[keyIdx])
}
// SetupJSONHelpers adds JSON-specific helper functions to Lua
func (p *JSONProcessor) SetupJSONHelpers(L *lua.LState) {
// Helper to get type of JSON value
L.SetGlobal("json_type", L.NewFunction(func(L *lua.LState) int {
// Get the value passed to the function
val := L.Get(1)
// Determine type
switch val.Type() {
case lua.LTNil:
L.Push(lua.LString("null"))
case lua.LTBool:
L.Push(lua.LString("boolean"))
case lua.LTNumber:
L.Push(lua.LString("number"))
case lua.LTString:
L.Push(lua.LString("string"))
case lua.LTTable:
// Could be object or array - check for numeric keys
isArray := true
table := val.(*lua.LTable)
table.ForEach(func(key, value lua.LValue) {
if key.Type() != lua.LTNumber {
isArray = false
}
})
if isArray {
L.Push(lua.LString("array"))
} else {
L.Push(lua.LString("object"))
}
default:
L.Push(lua.LString("unknown"))
}
return 1
}))
}
// jsonToLua converts a Go JSON value to a Lua value
func (p *JSONProcessor) jsonToLua(L *lua.LState, val interface{}) lua.LValue {
if val == nil {
return lua.LNil
}
switch v := val.(type) {
case bool:
return lua.LBool(v)
case float64:
return lua.LNumber(v)
case string:
return lua.LString(v)
case []interface{}:
arr := L.NewTable()
for i, item := range v {
arr.RawSetInt(i+1, p.jsonToLua(L, item))
}
return arr
case map[string]interface{}:
obj := L.NewTable()
for k, item := range v {
obj.RawSetString(k, p.jsonToLua(L, item))
}
return obj
default:
// For unknown types, convert to string representation
return lua.LString(fmt.Sprintf("%v", val))
}
}
// luaToJSON converts a Lua value to a Go JSON-compatible value
func (p *JSONProcessor) luaToJSON(val lua.LValue) interface{} {
switch val.Type() {
case lua.LTNil:
return nil
case lua.LTBool:
return lua.LVAsBool(val)
case lua.LTNumber:
return float64(val.(lua.LNumber))
case lua.LTString:
return val.String()
case lua.LTTable:
table := val.(*lua.LTable)
// Check if it's an array or an object
isArray := true
maxN := 0
table.ForEach(func(key, _ lua.LValue) {
if key.Type() == lua.LTNumber {
n := int(key.(lua.LNumber))
if n > maxN {
maxN = n
}
} else {
isArray = false
}
})
if isArray && maxN > 0 {
// It's an array
arr := make([]interface{}, maxN)
for i := 1; i <= maxN; i++ {
item := table.RawGetInt(i)
if item != lua.LNil {
arr[i-1] = p.luaToJSON(item)
}
}
return arr
} else {
// It's an object
obj := make(map[string]interface{})
table.ForEach(func(key, value lua.LValue) {
if key.Type() == lua.LTString {
obj[key.String()] = p.luaToJSON(value)
} else {
// Convert key to string if it's not already
obj[fmt.Sprintf("%v", key)] = p.luaToJSON(value)
}
})
return obj
}
default:
// For functions, userdata, etc., convert to string
return val.String()
}
}