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