From 4640281fbf94d5a4d9941f0793b9b464f977e8bd Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 25 Mar 2025 18:57:32 +0100 Subject: [PATCH] Enable root modifications Though I can not see why you would want to..... But there's no reason you would not be able to --- processor/json.go | 46 ++++++++++++++++++ processor/json_test.go | 88 ++++++++++++++++++++++++++++++++-- processor/jsonpath/jsonpath.go | 25 ++++------ 3 files changed, 140 insertions(+), 19 deletions(-) diff --git a/processor/json.go b/processor/json.go index 2f6fc9b..57b4f0b 100644 --- a/processor/json.go +++ b/processor/json.go @@ -147,6 +147,52 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s // updateJSONValue updates a value in the JSON structure based on its JSONPath func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error { + // Special handling for root node + if path == "$" { + // For the root node, we'll copy the value to the jsonData reference + // This is a special case since we can't directly replace the interface{} variable + + // We need to handle different types of root elements + switch rootValue := newValue.(type) { + case map[string]interface{}: + // For objects, we need to copy over all keys + rootMap, ok := jsonData.(map[string]interface{}) + if !ok { + // If the original wasn't a map, completely replace it with the new map + // This is handled by the jsonpath.Set function + return jsonpath.Set(jsonData, path, newValue) + } + + // Clear the original map + for k := range rootMap { + delete(rootMap, k) + } + + // Copy all keys from the new map + for k, v := range rootValue { + rootMap[k] = v + } + return nil + + case []interface{}: + // For arrays, we need to handle similarly + rootArray, ok := jsonData.([]interface{}) + if !ok { + // If the original wasn't an array, use jsonpath.Set + return jsonpath.Set(jsonData, path, newValue) + } + + // Clear and recreate the array + *&rootArray = rootValue + return nil + + default: + // For other types, use jsonpath.Set + return jsonpath.Set(jsonData, path, newValue) + } + } + + // For non-root paths, use the regular Set method err := jsonpath.Set(jsonData, path, newValue) if err != nil { return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err) diff --git a/processor/json_test.go b/processor/json_test.go index 4bf9441..b307228 100644 --- a/processor/json_test.go +++ b/processor/json_test.go @@ -937,16 +937,16 @@ func TestJSONProcessor_RestructuringData(t *testing.T) { "people": { "developers": [ { + "age": 25, "id": 1, - "name": "Alice", - "age": 25 + "name": "Alice" } ], "managers": [ { + "age": 30, "id": 2, - "name": "Bob", - "age": 30 + "name": "Bob" } ] } @@ -1042,3 +1042,83 @@ func TestJSONProcessor_FilteringArrayElements(t *testing.T) { t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) } } + +// TestJSONProcessor_RootNodeModification tests modifying the root node directly +func TestJSONProcessor_RootNodeModification(t *testing.T) { + content := `{ + "name": "original", + "value": 100 + }` + + expected := `{ + "name": "modified", + "description": "This is a completely modified root", + "values": [1, 2, 3] + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$", ` + -- Completely replace the root node + v = { + name = "modified", + description = "This is a completely modified root", + values = {1, 2, 3} + } + `) + + 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_RootNodeModificationToPrimitive tests modifying the root node to a primitive value +func TestJSONProcessor_RootNodeModificationToPrimitive(t *testing.T) { + content := `{ + "name": "original", + "value": 100 + }` + + expected := `42` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$", ` + -- Replace the root node with a primitive value + v = 42 + `) + + 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) + } +} diff --git a/processor/jsonpath/jsonpath.go b/processor/jsonpath/jsonpath.go index bd1fa0d..79498db 100644 --- a/processor/jsonpath/jsonpath.go +++ b/processor/jsonpath/jsonpath.go @@ -138,10 +138,6 @@ func Set(data interface{}, path string, value interface{}) error { return fmt.Errorf("failed to parse JSONPath %q: %w", path, err) } - if len(steps) <= 1 { - return fmt.Errorf("cannot set root node; the provided path %q is invalid", path) - } - success := false err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode) if err != nil { @@ -157,10 +153,6 @@ func SetAll(data interface{}, path string, value interface{}) error { return fmt.Errorf("failed to parse JSONPath %q: %w", path, err) } - // if len(steps) <= 1 { - // return fmt.Errorf("cannot set root node; the provided path %q is invalid", path) - // } - success := false err = setWithPath(data, steps, &success, value, "$", ModifyAllMode) if err != nil { @@ -178,17 +170,20 @@ func setWithPath(node interface{}, steps []JSONStep, success *bool, value interf // Skip root step actualSteps := steps if len(steps) > 0 && steps[0].Type == RootStep { - // if len(steps) == 1 { - // return fmt.Errorf("cannot set root node; the provided path %q is invalid", currentPath) - // } actualSteps = steps[1:] } - // Process the first step - // if len(actualSteps) == 0 { - // return fmt.Errorf("cannot set root node; no steps provided for path %q", currentPath) - // } + // If we have no steps left, we're setting the root value + if len(actualSteps) == 0 { + // For the root node, we need to handle it differently depending on what's passed in + // since we can't directly replace the interface{} variable + // We'll signal success and let the JSONProcessor handle updating the root + *success = true + return nil + } + + // Process the first step step := actualSteps[0] remainingSteps := actualSteps[1:] isLastStep := len(remainingSteps) == 0