512 lines
14 KiB
Go
512 lines
14 KiB
Go
package processor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestJSONProcessor_Process_NumericValues tests processing numeric JSON values
|
|
func TestJSONProcessor_Process_NumericValues(t *testing.T) {
|
|
// Test JSON with numeric price values we want to modify
|
|
testJSON := `{
|
|
"catalog": {
|
|
"books": [
|
|
{
|
|
"id": "bk101",
|
|
"author": "Gambardella, Matthew",
|
|
"title": "JSON Developer's Guide",
|
|
"genre": "Computer",
|
|
"price": 44.95,
|
|
"publish_date": "2000-10-01"
|
|
},
|
|
{
|
|
"id": "bk102",
|
|
"author": "Ralls, Kim",
|
|
"title": "Midnight Rain",
|
|
"genre": "Fantasy",
|
|
"price": 5.95,
|
|
"publish_date": "2000-12-16"
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Process the JSON content directly to double all prices
|
|
jsonPathExpr := "$.catalog.books[*].price"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, "v1 = v1 * 2", "*2")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check that we found and modified the correct number of nodes
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected to match 2 nodes, got %d", matchCount)
|
|
}
|
|
if modCount != 2 {
|
|
t.Errorf("Expected to modify 2 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Parse the JSON to check values more precisely
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Failed to parse modified JSON: %v", err)
|
|
}
|
|
|
|
// Navigate to the books array
|
|
catalog, ok := result["catalog"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("No catalog object found in result")
|
|
}
|
|
books, ok := catalog["books"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("No books array found in catalog")
|
|
}
|
|
|
|
// Check that both books have their prices doubled
|
|
// Note: The JSON numbers might be parsed as float64
|
|
book1, ok := books[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("First book is not an object")
|
|
}
|
|
price1, ok := book1["price"].(float64)
|
|
if !ok {
|
|
t.Fatalf("Price of first book is not a number")
|
|
}
|
|
if price1 != 89.9 {
|
|
t.Errorf("Expected first book price to be 89.9, got %v", price1)
|
|
}
|
|
|
|
book2, ok := books[1].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Second book is not an object")
|
|
}
|
|
price2, ok := book2["price"].(float64)
|
|
if !ok {
|
|
t.Fatalf("Price of second book is not a number")
|
|
}
|
|
if price2 != 11.9 {
|
|
t.Errorf("Expected second book price to be 11.9, got %v", price2)
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_Process_StringValues tests processing string JSON values
|
|
func TestJSONProcessor_Process_StringValues(t *testing.T) {
|
|
// Test JSON with string values we want to modify
|
|
testJSON := `{
|
|
"config": {
|
|
"settings": [
|
|
{ "name": "maxUsers", "value": "100" },
|
|
{ "name": "timeout", "value": "30" },
|
|
{ "name": "retries", "value": "5" }
|
|
]
|
|
}
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Process the JSON content directly to double all numeric values
|
|
jsonPathExpr := "$.config.settings[*].value"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, "v1 = v1 * 2", "*2")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check that we found and modified the correct number of nodes
|
|
if matchCount != 3 {
|
|
t.Errorf("Expected to match 3 nodes, got %d", matchCount)
|
|
}
|
|
if modCount != 3 {
|
|
t.Errorf("Expected to modify 3 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Check that the string values were doubled
|
|
if !strings.Contains(modifiedJSON, `"value": "200"`) {
|
|
t.Errorf("Modified content does not contain updated value 200")
|
|
}
|
|
if !strings.Contains(modifiedJSON, `"value": "60"`) {
|
|
t.Errorf("Modified content does not contain updated value 60")
|
|
}
|
|
if !strings.Contains(modifiedJSON, `"value": "10"`) {
|
|
t.Errorf("Modified content does not contain updated value 10")
|
|
}
|
|
|
|
// Verify the JSON is valid after modification
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Modified JSON is not valid: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_FindNodes tests the JSONPath implementation
|
|
func TestJSONProcessor_FindNodes(t *testing.T) {
|
|
// Test simple JSONPath functionality
|
|
testCases := []struct {
|
|
name string
|
|
jsonData string
|
|
path string
|
|
expectLen int
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "Root element",
|
|
jsonData: `{"name": "root", "value": 100}`,
|
|
path: "$",
|
|
expectLen: 1,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Direct property",
|
|
jsonData: `{"name": "test", "value": 100}`,
|
|
path: "$.value",
|
|
expectLen: 1,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Array access",
|
|
jsonData: `{"items": [10, 20, 30]}`,
|
|
path: "$.items[1]",
|
|
expectLen: 1,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "All array elements",
|
|
jsonData: `{"items": [10, 20, 30]}`,
|
|
path: "$.items[*]",
|
|
expectLen: 3,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Nested property",
|
|
jsonData: `{"user": {"name": "John", "age": 30}}`,
|
|
path: "$.user.age",
|
|
expectLen: 1,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Array of objects",
|
|
jsonData: `{"users": [{"name": "John"}, {"name": "Jane"}]}`,
|
|
path: "$.users[*].name",
|
|
expectLen: 2,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Invalid path",
|
|
jsonData: `{"name": "test"}`,
|
|
path: "$.invalid[[", // Double bracket should cause an error
|
|
expectLen: 0,
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
processor := &JSONProcessor{Logger: &TestLogger{}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Parse the JSON data
|
|
var jsonDoc interface{}
|
|
if err := json.Unmarshal([]byte(tc.jsonData), &jsonDoc); err != nil {
|
|
t.Fatalf("Failed to parse test JSON: %v", err)
|
|
}
|
|
|
|
// Find nodes with the given path
|
|
nodes, err := processor.findNodePaths(jsonDoc, tc.path)
|
|
|
|
// Check error expectation
|
|
if tc.expectErr && err == nil {
|
|
t.Errorf("Expected error but got none")
|
|
}
|
|
if !tc.expectErr && err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Skip further checks if we expected an error
|
|
if tc.expectErr {
|
|
return
|
|
}
|
|
|
|
// Check the number of nodes found
|
|
if len(nodes) != tc.expectLen {
|
|
t.Errorf("Expected %d nodes, got %d", tc.expectLen, len(nodes))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_NestedModifications tests modifying nested JSON objects
|
|
func TestJSONProcessor_NestedModifications(t *testing.T) {
|
|
testJSON := `{
|
|
"company": {
|
|
"name": "ABC Corp",
|
|
"departments": [
|
|
{
|
|
"id": "dev",
|
|
"name": "Development",
|
|
"employees": [
|
|
{"id": 1, "name": "John Doe", "salary": 75000},
|
|
{"id": 2, "name": "Jane Smith", "salary": 82000}
|
|
]
|
|
},
|
|
{
|
|
"id": "sales",
|
|
"name": "Sales",
|
|
"employees": [
|
|
{"id": 3, "name": "Bob Johnson", "salary": 65000},
|
|
{"id": 4, "name": "Alice Brown", "salary": 68000}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Process the JSON to give everyone a 10% raise
|
|
jsonPathExpr := "$.company.departments[*].employees[*].salary"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, "v1 = v1 * 1.1", "10% raise")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check counts
|
|
if matchCount != 4 {
|
|
t.Errorf("Expected to match 4 salary nodes, got %d", matchCount)
|
|
}
|
|
if modCount != 4 {
|
|
t.Errorf("Expected to modify 4 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Parse the result to verify changes
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Failed to parse modified JSON: %v", err)
|
|
}
|
|
|
|
// Get company > departments
|
|
company := result["company"].(map[string]interface{})
|
|
departments := company["departments"].([]interface{})
|
|
|
|
// Check first department's first employee
|
|
dept1 := departments[0].(map[string]interface{})
|
|
employees1 := dept1["employees"].([]interface{})
|
|
emp1 := employees1[0].(map[string]interface{})
|
|
salary1 := emp1["salary"].(float64)
|
|
|
|
// Salary should be 75000 * 1.1 = 82500
|
|
if salary1 != 82500 {
|
|
t.Errorf("Expected first employee salary to be 82500, got %v", salary1)
|
|
}
|
|
|
|
// Check second department's second employee
|
|
dept2 := departments[1].(map[string]interface{})
|
|
employees2 := dept2["employees"].([]interface{})
|
|
emp4 := employees2[1].(map[string]interface{})
|
|
salary4 := emp4["salary"].(float64)
|
|
|
|
// Salary should be 68000 * 1.1 = 74800
|
|
if salary4 != 74800 {
|
|
t.Errorf("Expected fourth employee salary to be 74800, got %v", salary4)
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_ArrayManipulation tests modifying JSON arrays
|
|
func TestJSONProcessor_ArrayManipulation(t *testing.T) {
|
|
testJSON := `{
|
|
"dataPoints": [10, 20, 30, 40, 50]
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Process the JSON to normalize values (divide by max value)
|
|
jsonPathExpr := "$.dataPoints[*]"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, "v1 = v1 / 50", "normalize")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check counts
|
|
if matchCount != 5 {
|
|
t.Errorf("Expected to match 5 data points, got %d", matchCount)
|
|
}
|
|
if modCount != 5 {
|
|
t.Errorf("Expected to modify 5 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Parse the result to verify changes
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Failed to parse modified JSON: %v", err)
|
|
}
|
|
|
|
// Get the data points array
|
|
dataPoints := result["dataPoints"].([]interface{})
|
|
|
|
// Check values (should be divided by 50)
|
|
expectedValues := []float64{0.2, 0.4, 0.6, 0.8, 1.0}
|
|
for i, val := range dataPoints {
|
|
if val.(float64) != expectedValues[i] {
|
|
t.Errorf("Expected dataPoints[%d] to be %v, got %v", i, expectedValues[i], val)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_ConditionalModification tests applying changes only to certain elements
|
|
func TestJSONProcessor_ConditionalModification(t *testing.T) {
|
|
testJSON := `{
|
|
"products": [
|
|
{"id": "p1", "name": "Laptop", "price": 999.99, "discount": 0},
|
|
{"id": "p2", "name": "Headphones", "price": 59.99, "discount": 0},
|
|
{"id": "p3", "name": "Mouse", "price": 29.99, "discount": 0}
|
|
]
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Process: apply 10% discount to items over $50, 5% to others
|
|
luaScript := `
|
|
-- Get the path to find the parent (product)
|
|
local path = string.gsub(_PATH, ".discount$", "")
|
|
|
|
-- Custom logic based on price
|
|
local price = _PARENT.price
|
|
if price > 50 then
|
|
v1 = 0.1 -- 10% discount
|
|
else
|
|
v1 = 0.05 -- 5% discount
|
|
end
|
|
`
|
|
|
|
jsonPathExpr := "$.products[*].discount"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, luaScript, "apply discounts")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check counts
|
|
if matchCount != 3 {
|
|
t.Errorf("Expected to match 3 discount nodes, got %d", matchCount)
|
|
}
|
|
if modCount != 3 {
|
|
t.Errorf("Expected to modify 3 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Parse the result to verify changes
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Failed to parse modified JSON: %v", err)
|
|
}
|
|
|
|
// Get products array
|
|
products := result["products"].([]interface{})
|
|
|
|
// Laptop and Headphones should have 10% discount
|
|
laptop := products[0].(map[string]interface{})
|
|
headphones := products[1].(map[string]interface{})
|
|
mouse := products[2].(map[string]interface{})
|
|
|
|
if laptop["discount"].(float64) != 0.1 {
|
|
t.Errorf("Expected laptop discount to be 0.1, got %v", laptop["discount"])
|
|
}
|
|
|
|
if headphones["discount"].(float64) != 0.1 {
|
|
t.Errorf("Expected headphones discount to be 0.1, got %v", headphones["discount"])
|
|
}
|
|
|
|
if mouse["discount"].(float64) != 0.05 {
|
|
t.Errorf("Expected mouse discount to be 0.05, got %v", mouse["discount"])
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_ComplexScripts tests using more complex Lua scripts
|
|
func TestJSONProcessor_ComplexScripts(t *testing.T) {
|
|
testJSON := `{
|
|
"metrics": [
|
|
{"name": "CPU", "values": [45, 60, 75, 90, 80]},
|
|
{"name": "Memory", "values": [30, 40, 45, 50, 60]},
|
|
{"name": "Disk", "values": [20, 25, 30, 40, 50]}
|
|
]
|
|
}`
|
|
|
|
// Create a JSON processor
|
|
processor := NewJSONProcessor(&TestLogger{T: t})
|
|
|
|
// Apply a moving average transformation
|
|
luaScript := `
|
|
-- This script transforms an array using a moving average
|
|
local values = {}
|
|
local window = 3 -- window size
|
|
|
|
-- Get all the values as a table
|
|
for i = 1, 5 do
|
|
local element = _VALUE[i]
|
|
if element then
|
|
values[i] = element
|
|
end
|
|
end
|
|
|
|
-- Calculate moving averages
|
|
local result = {}
|
|
for i = 1, #values do
|
|
local sum = 0
|
|
local count = 0
|
|
|
|
-- Sum the window
|
|
for j = math.max(1, i-(window-1)/2), math.min(#values, i+(window-1)/2) do
|
|
sum = sum + values[j]
|
|
count = count + 1
|
|
end
|
|
|
|
-- Set the average
|
|
result[i] = sum / count
|
|
end
|
|
|
|
-- Update all values
|
|
for i = 1, #result do
|
|
_VALUE[i] = result[i]
|
|
end
|
|
`
|
|
|
|
jsonPathExpr := "$.metrics[*].values"
|
|
modifiedJSON, modCount, matchCount, err := processor.ProcessContent(testJSON, jsonPathExpr, luaScript, "moving average")
|
|
if err != nil {
|
|
t.Fatalf("Failed to process JSON content: %v", err)
|
|
}
|
|
|
|
// Check counts
|
|
if matchCount != 3 {
|
|
t.Errorf("Expected to match 3 value arrays, got %d", matchCount)
|
|
}
|
|
if modCount != 3 {
|
|
t.Errorf("Expected to modify 3 nodes, got %d", modCount)
|
|
}
|
|
|
|
// Parse and verify the values were smoothed
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(modifiedJSON), &result); err != nil {
|
|
t.Fatalf("Failed to parse modified JSON: %v", err)
|
|
}
|
|
|
|
// The modification logic would smooth out the values
|
|
// We'll check that the JSON is valid at least
|
|
metrics := result["metrics"].([]interface{})
|
|
if len(metrics) != 3 {
|
|
t.Errorf("Expected 3 metrics, got %d", len(metrics))
|
|
}
|
|
|
|
// Each metrics should have 5 values
|
|
for i, metric := range metrics {
|
|
m := metric.(map[string]interface{})
|
|
values := m["values"].([]interface{})
|
|
if len(values) != 5 {
|
|
t.Errorf("Metric %d should have 5 values, got %d", i, len(values))
|
|
}
|
|
}
|
|
}
|