Files
event-driven-shoppinglist/json_patch.go
2025-09-29 08:14:53 +02:00

300 lines
8.3 KiB
Go

package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// JSONPatcher implements RFC6902 JSON Patch operations
type JSONPatcher struct{}
// ApplyPatches applies a series of JSON Patch operations to a document
func (jp *JSONPatcher) ApplyPatches(doc map[string]interface{}, patches []PatchOperation) (map[string]interface{}, error) {
// Apply patches in-place to avoid memory duplication
for i, patch := range patches {
var err error
doc, err = jp.applyPatch(doc, patch)
if err != nil {
return nil, fmt.Errorf("failed to apply patch %d: %w", i, err)
}
}
return doc, nil
}
// applyPatch applies a single JSON Patch operation
func (jp *JSONPatcher) applyPatch(doc map[string]interface{}, patch PatchOperation) (map[string]interface{}, error) {
switch patch.Op {
case "add":
return jp.applyAdd(doc, patch.Path, patch.Value)
case "remove":
return jp.applyRemove(doc, patch.Path)
case "replace":
return jp.applyReplace(doc, patch.Path, patch.Value)
case "move":
return jp.applyMove(doc, patch.From, patch.Path)
case "copy":
return jp.applyCopy(doc, patch.From, patch.Path)
case "test":
return jp.applyTest(doc, patch.Path, patch.Value)
default:
return nil, fmt.Errorf("unsupported operation: %s", patch.Op)
}
}
// applyAdd implements the "add" operation
func (jp *JSONPatcher) applyAdd(doc map[string]interface{}, path string, value interface{}) (map[string]interface{}, error) {
if path == "" {
// Adding to root replaces entire document
if newDoc, ok := value.(map[string]interface{}); ok {
return newDoc, nil
}
return nil, fmt.Errorf("cannot replace root with non-object")
}
parts := jp.parsePath(path)
if len(parts) == 0 {
return nil, fmt.Errorf("invalid path: %s", path)
}
// Navigate to parent and add the value
parent, key, err := jp.navigateToParent(doc, parts)
if err != nil {
return nil, err
}
if parent == nil {
return nil, fmt.Errorf("parent does not exist for path: %s", path)
}
if parentMap, ok := parent.(map[string]interface{}); ok {
parentMap[key] = value
} else if parentSlice, ok := parent.([]interface{}); ok {
index, err := strconv.Atoi(key)
if err != nil {
if key == "-" {
// Append to end - need to modify the parent reference
return nil, fmt.Errorf("array append operation not fully supported in this simplified implementation")
} else {
return nil, fmt.Errorf("invalid array index: %s", key)
}
} else {
// Insert at index - simplified implementation
if index < 0 || index > len(parentSlice) {
return nil, fmt.Errorf("array index out of bounds: %d", index)
}
// For simplicity, we'll replace at index if it exists, or error if beyond bounds
if index < len(parentSlice) {
parentSlice[index] = value
} else {
return nil, fmt.Errorf("array insertion beyond bounds not supported in simplified implementation")
}
}
} else {
return nil, fmt.Errorf("cannot add to non-object/non-array")
}
return doc, nil
}
// applyRemove implements the "remove" operation
func (jp *JSONPatcher) applyRemove(doc map[string]interface{}, path string) (map[string]interface{}, error) {
parts := jp.parsePath(path)
if len(parts) == 0 {
return nil, fmt.Errorf("cannot remove root")
}
parent, key, err := jp.navigateToParent(doc, parts)
if err != nil {
return nil, err
}
if parentMap, ok := parent.(map[string]interface{}); ok {
if _, exists := parentMap[key]; !exists {
return nil, fmt.Errorf("path does not exist: %s", path)
}
delete(parentMap, key)
} else if parentSlice, ok := parent.([]interface{}); ok {
index, err := strconv.Atoi(key)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", key)
}
if index < 0 || index >= len(parentSlice) {
return nil, fmt.Errorf("array index out of bounds: %d", index)
}
// Simplified remove - set to nil instead of actually removing
parentSlice[index] = nil
} else {
return nil, fmt.Errorf("cannot remove from non-object/non-array")
}
return doc, nil
}
// applyReplace implements the "replace" operation
func (jp *JSONPatcher) applyReplace(doc map[string]interface{}, path string, value interface{}) (map[string]interface{}, error) {
if path == "" {
// Replace entire document
if newDoc, ok := value.(map[string]interface{}); ok {
return newDoc, nil
}
return nil, fmt.Errorf("cannot replace root with non-object")
}
parts := jp.parsePath(path)
parent, key, err := jp.navigateToParent(doc, parts)
if err != nil {
return nil, err
}
if parentMap, ok := parent.(map[string]interface{}); ok {
if _, exists := parentMap[key]; !exists {
return nil, fmt.Errorf("path does not exist: %s", path)
}
parentMap[key] = value
} else if parentSlice, ok := parent.([]interface{}); ok {
index, err := strconv.Atoi(key)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", key)
}
if index < 0 || index >= len(parentSlice) {
return nil, fmt.Errorf("array index out of bounds: %d", index)
}
parentSlice[index] = value
} else {
return nil, fmt.Errorf("cannot replace in non-object/non-array")
}
return doc, nil
}
// applyMove implements the "move" operation
func (jp *JSONPatcher) applyMove(doc map[string]interface{}, from, to string) (map[string]interface{}, error) {
// Get value from source
value, err := jp.getValue(doc, from)
if err != nil {
return nil, fmt.Errorf("move source not found: %w", err)
}
// Remove from source
doc, err = jp.applyRemove(doc, from)
if err != nil {
return nil, fmt.Errorf("failed to remove from source: %w", err)
}
// Add to destination
doc, err = jp.applyAdd(doc, to, value)
if err != nil {
return nil, fmt.Errorf("failed to add to destination: %w", err)
}
return doc, nil
}
// applyCopy implements the "copy" operation
func (jp *JSONPatcher) applyCopy(doc map[string]interface{}, from, to string) (map[string]interface{}, error) {
// Get value from source
value, err := jp.getValue(doc, from)
if err != nil {
return nil, fmt.Errorf("copy source not found: %w", err)
}
// Add to destination
doc, err = jp.applyAdd(doc, to, value)
if err != nil {
return nil, fmt.Errorf("failed to add to destination: %w", err)
}
return doc, nil
}
// applyTest implements the "test" operation
func (jp *JSONPatcher) applyTest(doc map[string]interface{}, path string, expectedValue interface{}) (map[string]interface{}, error) {
actualValue, err := jp.getValue(doc, path)
if err != nil {
return nil, fmt.Errorf("test path not found: %w", err)
}
if !reflect.DeepEqual(actualValue, expectedValue) {
return nil, fmt.Errorf("test failed: expected %v, got %v", expectedValue, actualValue)
}
return doc, nil
}
// getValue retrieves a value at the given JSON Pointer path
func (jp *JSONPatcher) getValue(doc map[string]interface{}, path string) (interface{}, error) {
if path == "" {
return doc, nil
}
parts := jp.parsePath(path)
current := interface{}(doc)
for _, part := range parts {
if currentMap, ok := current.(map[string]interface{}); ok {
var exists bool
current, exists = currentMap[part]
if !exists {
return nil, fmt.Errorf("path not found: %s", path)
}
} else if currentSlice, ok := current.([]interface{}); ok {
index, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", part)
}
if index < 0 || index >= len(currentSlice) {
return nil, fmt.Errorf("array index out of bounds: %d", index)
}
current = currentSlice[index]
} else {
return nil, fmt.Errorf("cannot navigate through non-object/non-array")
}
}
return current, nil
}
// navigateToParent navigates to the parent of the target path
func (jp *JSONPatcher) navigateToParent(doc map[string]interface{}, parts []string) (interface{}, string, error) {
if len(parts) == 0 {
return nil, "", fmt.Errorf("no parent for root")
}
if len(parts) == 1 {
return doc, parts[0], nil
}
parentPath := "/" + strings.Join(parts[:len(parts)-1], "/")
parent, err := jp.getValue(doc, parentPath)
if err != nil {
return nil, "", err
}
return parent, parts[len(parts)-1], nil
}
// parsePath parses a JSON Pointer path into parts
func (jp *JSONPatcher) parsePath(path string) []string {
if path == "" {
return []string{}
}
if !strings.HasPrefix(path, "/") {
return []string{}
}
parts := strings.Split(path[1:], "/")
// Unescape JSON Pointer characters
for i, part := range parts {
part = strings.ReplaceAll(part, "~1", "/")
part = strings.ReplaceAll(part, "~0", "~")
parts[i] = part
}
return parts
}