1772 lines
39 KiB
Go
1772 lines
39 KiB
Go
package processor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/PaesslerAG/jsonpath"
|
|
)
|
|
|
|
// fi ndMatchingPaths finds nodes in a JSON document that match the given JSONPath
|
|
func findMatchingPaths(jsonDoc interface{}, path string) ([]interface{}, error) {
|
|
// Use the existing jsonpath library to extract values
|
|
result, err := jsonpath.Get(path, jsonDoc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert the result to a slice
|
|
var values []interface{}
|
|
switch v := result.(type) {
|
|
case []interface{}:
|
|
values = v
|
|
default:
|
|
values = []interface{}{v}
|
|
}
|
|
|
|
return values, nil
|
|
}
|
|
|
|
// TestJSONProcessor_Process_NumericValues tests processing numeric JSON values
|
|
func TestJSONProcessor_Process_NumericValues(t *testing.T) {
|
|
content := `{
|
|
"books": [
|
|
{
|
|
"title": "The Go Programming Language",
|
|
"price": 44.95
|
|
},
|
|
{
|
|
"title": "Go in Action",
|
|
"price": 5.95
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"books": [
|
|
{
|
|
"title": "The Go Programming Language",
|
|
"price": 89.9
|
|
},
|
|
{
|
|
"title": "Go in Action",
|
|
"price": 11.9
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.books[*]", "v.price=v.price*2")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, got %d", modCount)
|
|
}
|
|
|
|
// Compare parsed JSON objects instead of formatted strings
|
|
var resultObj map[string]interface{}
|
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
|
}
|
|
|
|
var expectedObj map[string]interface{}
|
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
|
}
|
|
|
|
// Compare the first book's price
|
|
resultBooks, ok := resultObj["books"].([]interface{})
|
|
if !ok || len(resultBooks) < 1 {
|
|
t.Fatalf("Expected books array in result")
|
|
}
|
|
resultBook1, ok := resultBooks[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected first book to be an object")
|
|
}
|
|
resultPrice1, ok := resultBook1["price"].(float64)
|
|
if !ok {
|
|
t.Fatalf("Expected numeric price in first book")
|
|
}
|
|
if resultPrice1 != 89.9 {
|
|
t.Errorf("Expected first book price to be 89.9, got %v", resultPrice1)
|
|
}
|
|
|
|
// Compare the second book's price
|
|
resultBook2, ok := resultBooks[1].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected second book to be an object")
|
|
}
|
|
resultPrice2, ok := resultBook2["price"].(float64)
|
|
if !ok {
|
|
t.Fatalf("Expected numeric price in second book")
|
|
}
|
|
if resultPrice2 != 11.9 {
|
|
t.Errorf("Expected second book price to be 11.9, got %v", resultPrice2)
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_Process_StringValues tests processing string JSON values
|
|
func TestJSONProcessor_Process_StringValues(t *testing.T) {
|
|
content := `{
|
|
"config": {
|
|
"maxItems": "100",
|
|
"itemTimeoutSecs": "30",
|
|
"retryCount": "5"
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.config", "for k,vi in pairs(v) do v[k]=vi*2 end")
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
// Debug info
|
|
t.Logf("Result: %s", result)
|
|
t.Logf("Match count: %d, Mod count: %d", matchCount, modCount)
|
|
|
|
if matchCount != 1 {
|
|
t.Errorf("Expected 1 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 1 {
|
|
t.Errorf("Expected 1 modifications, got %d", modCount)
|
|
}
|
|
|
|
// Check that all expected values are in the result
|
|
if !strings.Contains(result, `"maxItems": 200`) {
|
|
t.Errorf("Result missing expected value: maxItems=200")
|
|
}
|
|
|
|
if !strings.Contains(result, `"itemTimeoutSecs": 60`) {
|
|
t.Errorf("Result missing expected value: itemTimeoutSecs=60")
|
|
}
|
|
|
|
if !strings.Contains(result, `"retryCount": 10`) {
|
|
t.Errorf("Result missing expected value: retryCount=10")
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_FindNodes tests the JSONPath implementation
|
|
func TestJSONProcessor_FindNodes(t *testing.T) {
|
|
// Test cases for JSONPath implementation
|
|
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,
|
|
},
|
|
}
|
|
|
|
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 := findMatchingPaths(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) {
|
|
content := `{
|
|
"store": {
|
|
"book": [
|
|
{
|
|
"category": "reference",
|
|
"title": "Learn Go in 24 Hours",
|
|
"price": 10.99
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"title": "The Go Developer",
|
|
"price": 8.99
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"store": {
|
|
"book": [
|
|
{
|
|
"category": "reference",
|
|
"price": 13.188,
|
|
"title": "Learn Go in 24 Hours"
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"price": 10.788,
|
|
"title": "The Go Developer"
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.store.book[*].price", "v=v*1.2")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_StringManipulation tests string manipulation
|
|
func TestJSONProcessor_StringManipulation(t *testing.T) {
|
|
content := `{
|
|
"users": [
|
|
{
|
|
"name": "john",
|
|
"role": "admin"
|
|
},
|
|
{
|
|
"name": "alice",
|
|
"role": "user"
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"users": [
|
|
{
|
|
"name": "JOHN",
|
|
"role": "admin"
|
|
},
|
|
{
|
|
"name": "ALICE",
|
|
"role": "user"
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.users[*].name", "v = string.upper(v)")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_ComplexScript tests using more complex Lua scripts
|
|
func TestJSONProcessor_ComplexScript(t *testing.T) {
|
|
content := `{
|
|
"products": [
|
|
{
|
|
"name": "Basic Widget",
|
|
"price": 9.99,
|
|
"discount": 0.1
|
|
},
|
|
{
|
|
"name": "Premium Widget",
|
|
"price": 19.99,
|
|
"discount": 0.05
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"products": [
|
|
{
|
|
"discount": 0.1,
|
|
"name": "Basic Widget",
|
|
"price": 8.991
|
|
},
|
|
{
|
|
"discount": 0.05,
|
|
"name": "Premium Widget",
|
|
"price": 18.9905
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", "v.price = round(v.price * (1 - v.discount), 4)")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_SpecificItemUpdate tests updating a specific item in an array
|
|
func TestJSONProcessor_SpecificItemUpdate(t *testing.T) {
|
|
content := `{
|
|
"items": [
|
|
{"id": 1, "name": "Item 1", "stock": 10},
|
|
{"id": 2, "name": "Item 2", "stock": 5},
|
|
{"id": 3, "name": "Item 3", "stock": 0}
|
|
]
|
|
}`
|
|
|
|
expected := `
|
|
{
|
|
"items": [
|
|
{
|
|
"id": 1,
|
|
"name": "Item 1",
|
|
"stock": 10
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Item 2",
|
|
"stock": 15
|
|
},
|
|
{
|
|
"id": 3,
|
|
"name": "Item 3",
|
|
"stock": 0
|
|
}
|
|
]
|
|
} `
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.items[1].stock", "v=v+10")
|
|
|
|
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_RootElementUpdate tests updating the root element
|
|
func TestJSONProcessor_RootElementUpdate(t *testing.T) {
|
|
content := `{"value": 100}`
|
|
expected := `{
|
|
"value": 200
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.value", "v=v*2")
|
|
|
|
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_AddNewField tests adding a new field to a JSON object
|
|
func TestJSONProcessor_AddNewField(t *testing.T) {
|
|
content := `{
|
|
"user": {
|
|
"name": "John",
|
|
"age": 30
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"user": {
|
|
"age": 30,
|
|
"email": "john@example.com",
|
|
"name": "John"
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.user", "v.email = 'john@example.com'")
|
|
|
|
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_RemoveField tests removing a field from a JSON object
|
|
func TestJSONProcessor_RemoveField(t *testing.T) {
|
|
content := `{
|
|
"user": {
|
|
"name": "John",
|
|
"age": 30,
|
|
"email": "john@example.com"
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"user": {
|
|
"email": "john@example.com",
|
|
"name": "John"
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.user", "v.age = nil")
|
|
|
|
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_ArrayManipulation tests adding and manipulating array elements
|
|
func TestJSONProcessor_ArrayManipulation(t *testing.T) {
|
|
content := `{
|
|
"tags": ["go", "json", "lua"]
|
|
}`
|
|
|
|
expected := ` {
|
|
"tags": [
|
|
"GO",
|
|
"JSON",
|
|
"LUA",
|
|
"testing"
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.tags", `
|
|
-- Convert existing tags to uppercase
|
|
for i=1, #v do
|
|
v[i] = string.upper(v[i])
|
|
end
|
|
-- Add a new tag
|
|
v[#v+1] = "testing"
|
|
`)
|
|
|
|
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_ConditionalModification tests conditionally modifying values
|
|
func TestJSONProcessor_ConditionalModification(t *testing.T) {
|
|
content := `{
|
|
"products": [
|
|
{
|
|
"name": "Product A",
|
|
"price": 10.99,
|
|
"inStock": true
|
|
},
|
|
{
|
|
"name": "Product B",
|
|
"price": 5.99,
|
|
"inStock": false
|
|
},
|
|
{
|
|
"name": "Product C",
|
|
"price": 15.99,
|
|
"inStock": true
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"products": [
|
|
{
|
|
"inStock": true,
|
|
"name": "Product A",
|
|
"price": 9.891
|
|
},
|
|
{
|
|
"inStock": false,
|
|
"name": "Product B",
|
|
"price": 5.99
|
|
},
|
|
{
|
|
"inStock": true,
|
|
"name": "Product C",
|
|
"price": 14.391
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.products[*]", `
|
|
if not v.inStock then
|
|
return false
|
|
end
|
|
v.price = v.price * 0.9
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 3 {
|
|
t.Errorf("Expected 3 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_DeepNesting tests manipulating deeply nested JSON structures
|
|
func TestJSONProcessor_DeepNesting(t *testing.T) {
|
|
content := `{
|
|
"company": {
|
|
"departments": {
|
|
"engineering": {
|
|
"teams": {
|
|
"frontend": {
|
|
"members": 12,
|
|
"projects": 5
|
|
},
|
|
"backend": {
|
|
"members": 8,
|
|
"projects": 3
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"company": {
|
|
"departments": {
|
|
"engineering": {
|
|
"teams": {
|
|
"backend": {
|
|
"members": 8,
|
|
"projects": 3,
|
|
"status": "active"
|
|
},
|
|
"frontend": {
|
|
"members": 12,
|
|
"projects": 5,
|
|
"status": "active"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.company.departments.engineering.teams.*", `
|
|
v.status = "active"
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_ComplexTransformation tests a complex transformation involving
|
|
// multiple fields and calculations
|
|
func TestJSONProcessor_ComplexTransformation(t *testing.T) {
|
|
content := `{
|
|
"order": {
|
|
"items": [
|
|
{
|
|
"product": "Widget A",
|
|
"quantity": 5,
|
|
"price": 10.0
|
|
},
|
|
{
|
|
"product": "Widget B",
|
|
"quantity": 3,
|
|
"price": 15.0
|
|
}
|
|
],
|
|
"customer": {
|
|
"name": "John Smith",
|
|
"tier": "gold"
|
|
}
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"order": {
|
|
"customer": {
|
|
"name": "John Smith",
|
|
"tier": "gold"
|
|
},
|
|
"items": [
|
|
{
|
|
"discounted_total": 45,
|
|
"price": 10,
|
|
"product": "Widget A",
|
|
"quantity": 5,
|
|
"total": 50
|
|
},
|
|
{
|
|
"discounted_total": 40.5,
|
|
"price": 15,
|
|
"product": "Widget B",
|
|
"quantity": 3,
|
|
"total": 45
|
|
}
|
|
],
|
|
"summary": {
|
|
"discount": 9.5,
|
|
"subtotal": 95,
|
|
"total": 85.5,
|
|
"total_items": 8
|
|
}
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.order", `
|
|
-- Calculate item totals and apply discounts
|
|
local discount_rate = 0.1 -- 10% discount for gold tier
|
|
local subtotal = 0
|
|
local total_items = 0
|
|
|
|
for i, item in ipairs(v.items) do
|
|
-- Calculate item total
|
|
item.total = item.quantity * item.price
|
|
|
|
-- Apply discount
|
|
item.discounted_total = item.total * (1 - discount_rate)
|
|
|
|
-- Add to running totals
|
|
subtotal = subtotal + item.total
|
|
total_items = total_items + item.quantity
|
|
end
|
|
|
|
-- Add order summary
|
|
v.summary = {
|
|
total_items = total_items,
|
|
subtotal = subtotal,
|
|
discount = subtotal * discount_rate,
|
|
total = subtotal * (1 - discount_rate)
|
|
}
|
|
`)
|
|
|
|
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_HandlingNullValues tests handling of null values in JSON
|
|
func TestJSONProcessor_HandlingNullValues(t *testing.T) {
|
|
content := `{
|
|
"data": {
|
|
"value1": null,
|
|
"value2": 42,
|
|
"value3": "hello"
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"data": {
|
|
"value1": 0,
|
|
"value2": 42,
|
|
"value3": "hello"
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.data.value1", `
|
|
v = 0
|
|
`)
|
|
|
|
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_RestructuringData tests completely restructuring JSON data
|
|
func TestJSONProcessor_RestructuringData(t *testing.T) {
|
|
content := `{
|
|
"people": [
|
|
{
|
|
"id": 1,
|
|
"name": "Alice",
|
|
"attributes": {
|
|
"age": 25,
|
|
"role": "developer"
|
|
}
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Bob",
|
|
"attributes": {
|
|
"age": 30,
|
|
"role": "manager"
|
|
}
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"people": {
|
|
"developers": [
|
|
{
|
|
"age": 25,
|
|
"id": 1,
|
|
"name": "Alice"
|
|
}
|
|
],
|
|
"managers": [
|
|
{
|
|
"age": 30,
|
|
"id": 2,
|
|
"name": "Bob"
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$", `
|
|
-- Restructure the data
|
|
local old_people = v.people
|
|
local new_structure = {
|
|
developers = {},
|
|
managers = {}
|
|
}
|
|
|
|
for _, person in ipairs(old_people) do
|
|
local role = person.attributes.role
|
|
local new_person = {
|
|
id = person.id,
|
|
name = person.name,
|
|
age = person.attributes.age
|
|
}
|
|
|
|
if role == "developer" then
|
|
table.insert(new_structure.developers, new_person)
|
|
elseif role == "manager" then
|
|
table.insert(new_structure.managers, new_person)
|
|
end
|
|
end
|
|
|
|
v.people = new_structure
|
|
`)
|
|
|
|
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_FilteringArrayElements tests filtering elements from an array
|
|
func TestJSONProcessor_FilteringArrayElements(t *testing.T) {
|
|
content := `{
|
|
"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
}`
|
|
|
|
expected := `{
|
|
"numbers": [
|
|
2,
|
|
4,
|
|
6,
|
|
8,
|
|
10
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.numbers", `
|
|
-- Filter to keep only even numbers
|
|
local filtered = {}
|
|
for _, num in ipairs(v) do
|
|
if num % 2 == 0 then
|
|
table.insert(filtered, num)
|
|
end
|
|
end
|
|
v = filtered
|
|
`)
|
|
|
|
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_RootNodeModification tests modifying the root node directly
|
|
func TestJSONProcessor_RootNodeModification(t *testing.T) {
|
|
content := `{
|
|
"name": "original",
|
|
"value": 100
|
|
}`
|
|
|
|
expected := `{
|
|
"description": "This is a completely modified root",
|
|
"name": "modified",
|
|
"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_DateManipulation tests manipulating date strings in a JSON document
|
|
func TestJSONProcessor_DateManipulation(t *testing.T) {
|
|
content := `{
|
|
"events": [
|
|
{
|
|
"name": "Conference",
|
|
"date": "2023-06-15"
|
|
},
|
|
{
|
|
"name": "Workshop",
|
|
"date": "2023-06-20"
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"events": [
|
|
{
|
|
"name": "Conference",
|
|
"date": "2023-07-15"
|
|
},
|
|
{
|
|
"name": "Workshop",
|
|
"date": "2023-07-20"
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.events[*].date", `
|
|
local year, month, day = string.match(v, "(%d%d%d%d)-(%d%d)-(%d%d)")
|
|
-- Postpone events by 1 month
|
|
month = tonumber(month) + 1
|
|
if month > 12 then
|
|
month = 1
|
|
year = tonumber(year) + 1
|
|
end
|
|
v = string.format("%04d-%02d-%s", tonumber(year), month, day)
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, got %d", modCount)
|
|
}
|
|
|
|
// Parse results as JSON objects for deep comparison rather than string comparison
|
|
var resultObj map[string]interface{}
|
|
var expectedObj map[string]interface{}
|
|
|
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
|
}
|
|
|
|
// Get the events arrays
|
|
resultEvents, ok := resultObj["events"].([]interface{})
|
|
if !ok || len(resultEvents) != 2 {
|
|
t.Fatalf("Expected events array with 2 items in result")
|
|
}
|
|
|
|
expectedEvents, ok := expectedObj["events"].([]interface{})
|
|
if !ok || len(expectedEvents) != 2 {
|
|
t.Fatalf("Expected events array with 2 items in expected")
|
|
}
|
|
|
|
// Check each event's date value
|
|
for i := 0; i < 2; i++ {
|
|
resultEvent, ok := resultEvents[i].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected event %d to be an object", i)
|
|
}
|
|
|
|
expectedEvent, ok := expectedEvents[i].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected expected event %d to be an object", i)
|
|
}
|
|
|
|
resultDate, ok := resultEvent["date"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected date in result event %d to be a string", i)
|
|
}
|
|
|
|
expectedDate, ok := expectedEvent["date"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected date in expected event %d to be a string", i)
|
|
}
|
|
|
|
if resultDate != expectedDate {
|
|
t.Errorf("Event %d: expected date %s, got %s", i, expectedDate, resultDate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_MathFunctions tests using math functions in JSON processing
|
|
func TestJSONProcessor_MathFunctions(t *testing.T) {
|
|
content := `{
|
|
"measurements": [
|
|
3.14159,
|
|
2.71828,
|
|
1.41421
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"measurements": [
|
|
3,
|
|
3,
|
|
1
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.measurements[*]", "v = round(v)")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 3 {
|
|
t.Errorf("Expected 3 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 3 {
|
|
t.Errorf("Expected 3 modifications, 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_Error_InvalidJSON tests error handling for invalid JSON
|
|
func TestJSONProcessor_Error_InvalidJSON(t *testing.T) {
|
|
content := `{
|
|
"unclosed": "value"
|
|
`
|
|
|
|
p := &JSONProcessor{}
|
|
_, _, _, err := p.ProcessContent(content, "$.unclosed", "v='modified'")
|
|
|
|
if err == nil {
|
|
t.Errorf("Expected an error for invalid JSON, but got none")
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_Error_InvalidJSONPath tests error handling for invalid JSONPath
|
|
func TestJSONProcessor_Error_InvalidJSONPath(t *testing.T) {
|
|
content := `{
|
|
"element": "value"
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
_, _, _, err := p.ProcessContent(content, "[invalid path]", "v='modified'")
|
|
|
|
if err == nil {
|
|
t.Errorf("Expected an error for invalid JSONPath, but got none")
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_Error_InvalidLua tests error handling for invalid Lua
|
|
func TestJSONProcessor_Error_InvalidLua(t *testing.T) {
|
|
content := `{
|
|
"element": 123
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
_, _, _, err := p.ProcessContent(content, "$.element", "v = invalid_function()")
|
|
|
|
if err == nil {
|
|
t.Errorf("Expected an error for invalid Lua, but got none")
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_Process_SpecialCharacters tests handling of special characters in JSON
|
|
func TestJSONProcessor_Process_SpecialCharacters(t *testing.T) {
|
|
content := `{
|
|
"data": [
|
|
"This & that",
|
|
"a < b",
|
|
"c > d",
|
|
"Quote: \"Hello\""
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.data[*]", "v = string.upper(v)")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 4 {
|
|
t.Errorf("Expected 4 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 4 {
|
|
t.Errorf("Expected 4 modifications, got %d", modCount)
|
|
}
|
|
|
|
// Parse the result to verify the content
|
|
var resultObj map[string]interface{}
|
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
|
}
|
|
|
|
data, ok := resultObj["data"].([]interface{})
|
|
if !ok || len(data) != 4 {
|
|
t.Fatalf("Expected data array with 4 items")
|
|
}
|
|
|
|
expectedValues := []string{
|
|
"THIS & THAT",
|
|
"A < B",
|
|
"C > D",
|
|
"QUOTE: \"HELLO\"",
|
|
}
|
|
|
|
for i, val := range data {
|
|
strVal, ok := val.(string)
|
|
if !ok {
|
|
t.Errorf("Expected item %d to be a string", i)
|
|
continue
|
|
}
|
|
|
|
if strVal != expectedValues[i] {
|
|
t.Errorf("Item %d: expected %q, got %q", i, expectedValues[i], strVal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_AggregateCalculation tests calculating aggregated values from multiple fields
|
|
func TestJSONProcessor_AggregateCalculation(t *testing.T) {
|
|
content := `{
|
|
"items": [
|
|
{
|
|
"name": "Apple",
|
|
"price": 1.99,
|
|
"quantity": 10
|
|
},
|
|
{
|
|
"name": "Carrot",
|
|
"price": 0.99,
|
|
"quantity": 5
|
|
}
|
|
]
|
|
}`
|
|
|
|
expected := `{
|
|
"items": [
|
|
{
|
|
"name": "Apple",
|
|
"price": 1.99,
|
|
"quantity": 10,
|
|
"total": 19.9
|
|
},
|
|
{
|
|
"name": "Carrot",
|
|
"price": 0.99,
|
|
"quantity": 5,
|
|
"total": 4.95
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.items[*]", `
|
|
-- Calculate total from price and quantity
|
|
local price = v.price
|
|
local quantity = v.quantity
|
|
|
|
-- Add new total field
|
|
v.total = price * quantity
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing content: %v", err)
|
|
}
|
|
|
|
if matchCount != 2 {
|
|
t.Errorf("Expected 2 matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 2 {
|
|
t.Errorf("Expected 2 modifications, 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_DataAnonymization tests anonymizing sensitive data
|
|
func TestJSONProcessor_DataAnonymization(t *testing.T) {
|
|
content := `{
|
|
"contacts": [
|
|
{
|
|
"name": "John Doe",
|
|
"email": "john.doe@example.com",
|
|
"phone": "123-456-7890"
|
|
},
|
|
{
|
|
"name": "Jane Smith",
|
|
"email": "jane.smith@example.com",
|
|
"phone": "456-789-0123"
|
|
}
|
|
]
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
|
|
// First pass: anonymize email addresses
|
|
result, modCount1, matchCount1, err := p.ProcessContent(content, "$.contacts[*].email", `
|
|
-- Anonymize email
|
|
v = string.gsub(v, "@.+", "@anon.com")
|
|
local username = string.match(v, "(.+)@")
|
|
v = string.gsub(username, "%.", "") .. "@anon.com"
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing email content: %v", err)
|
|
}
|
|
|
|
// Second pass: anonymize phone numbers
|
|
result, modCount2, matchCount2, err := p.ProcessContent(result, "$.contacts[*].phone", `
|
|
-- Mask phone numbers
|
|
v = string.gsub(v, "%d%d%d%-%d%d%d%-%d%d%d%d", function(match)
|
|
return string.sub(match, 1, 3) .. "-XXX-XXXX"
|
|
end)
|
|
`)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error processing phone content: %v", err)
|
|
}
|
|
|
|
// Total counts from both operations
|
|
matchCount := matchCount1 + matchCount2
|
|
modCount := modCount1 + modCount2
|
|
|
|
if matchCount != 4 {
|
|
t.Errorf("Expected 4 total matches, got %d", matchCount)
|
|
}
|
|
|
|
if modCount != 4 {
|
|
t.Errorf("Expected 4 total modifications, got %d", modCount)
|
|
}
|
|
|
|
// Parse the resulting JSON for validating content
|
|
var resultObj map[string]interface{}
|
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
|
}
|
|
|
|
contacts, ok := resultObj["contacts"].([]interface{})
|
|
if !ok || len(contacts) != 2 {
|
|
t.Fatalf("Expected contacts array with 2 items")
|
|
}
|
|
|
|
// Validate first contact
|
|
contact1, ok := contacts[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected first contact to be an object")
|
|
}
|
|
|
|
if email1, ok := contact1["email"].(string); !ok || email1 != "johndoe@anon.com" {
|
|
t.Errorf("First contact email should be johndoe@anon.com, got %v", contact1["email"])
|
|
}
|
|
|
|
if phone1, ok := contact1["phone"].(string); !ok || phone1 != "123-XXX-XXXX" {
|
|
t.Errorf("First contact phone should be 123-XXX-XXXX, got %v", contact1["phone"])
|
|
}
|
|
|
|
// Validate second contact
|
|
contact2, ok := contacts[1].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected second contact to be an object")
|
|
}
|
|
|
|
if email2, ok := contact2["email"].(string); !ok || email2 != "janesmith@anon.com" {
|
|
t.Errorf("Second contact email should be janesmith@anon.com, got %v", contact2["email"])
|
|
}
|
|
|
|
if phone2, ok := contact2["phone"].(string); !ok || phone2 != "456-XXX-XXXX" {
|
|
t.Errorf("Second contact phone should be 456-XXX-XXXX, got %v", contact2["phone"])
|
|
}
|
|
}
|
|
|
|
// TestJSONProcessor_ChainedOperations tests sequential operations on the same data
|
|
func TestJSONProcessor_ChainedOperations(t *testing.T) {
|
|
content := `{
|
|
"product": {
|
|
"name": "Widget",
|
|
"price": 100
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"product": {
|
|
"name": "Widget",
|
|
"price": 103.5
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$.product.price", `
|
|
-- When v is a numeric value, we can perform math operations directly
|
|
local price = v
|
|
-- Add 15% tax
|
|
price = price * 1.15
|
|
-- Apply 10% discount
|
|
price = price * 0.9
|
|
-- Round to 2 decimal places
|
|
price = math.floor(price * 100 + 0.5) / 100
|
|
v = price
|
|
`)
|
|
|
|
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_ComplexDataTransformation tests advanced JSON transformation
|
|
func TestJSONProcessor_ComplexDataTransformation(t *testing.T) {
|
|
content := `{
|
|
"store": {
|
|
"name": "My Store",
|
|
"inventory": [
|
|
{
|
|
"id": 1,
|
|
"name": "Laptop",
|
|
"category": "electronics",
|
|
"price": 999.99,
|
|
"stock": 15,
|
|
"features": ["16GB RAM", "512GB SSD", "15-inch display"]
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Smartphone",
|
|
"category": "electronics",
|
|
"price": 499.99,
|
|
"stock": 25,
|
|
"features": ["6GB RAM", "128GB storage", "5G"]
|
|
},
|
|
{
|
|
"id": 3,
|
|
"name": "T-Shirt",
|
|
"category": "clothing",
|
|
"price": 19.99,
|
|
"stock": 100,
|
|
"features": ["100% cotton", "M, L, XL sizes", "Red color"]
|
|
},
|
|
{
|
|
"id": 4,
|
|
"name": "Headphones",
|
|
"category": "electronics",
|
|
"price": 149.99,
|
|
"stock": 8,
|
|
"features": ["Noise cancelling", "Bluetooth", "20hr battery"]
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
expected := `{
|
|
"store": {
|
|
"name": "My Store",
|
|
"inventory_summary": {
|
|
"electronics": {
|
|
"count": 3,
|
|
"total_value": 30924.77,
|
|
"low_stock_items": [
|
|
{
|
|
"id": 4,
|
|
"name": "Headphones",
|
|
"stock": 8
|
|
}
|
|
]
|
|
},
|
|
"clothing": {
|
|
"count": 1,
|
|
"total_value": 1999.00,
|
|
"low_stock_items": []
|
|
}
|
|
},
|
|
"transformed_items": [
|
|
{
|
|
"name": "Laptop",
|
|
"price_with_tax": 1199.99,
|
|
"in_stock": true
|
|
},
|
|
{
|
|
"name": "Smartphone",
|
|
"price_with_tax": 599.99,
|
|
"in_stock": true
|
|
},
|
|
{
|
|
"name": "T-Shirt",
|
|
"price_with_tax": 23.99,
|
|
"in_stock": true
|
|
},
|
|
{
|
|
"name": "Headphones",
|
|
"price_with_tax": 179.99,
|
|
"in_stock": true
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
p := &JSONProcessor{}
|
|
|
|
// First, create a complex transformation that:
|
|
// 1. Summarizes inventory by category (count, total value, low stock alerts)
|
|
// 2. Creates a simplified view of items with tax added
|
|
result, modCount, matchCount, err := p.ProcessContent(content, "$", `
|
|
-- Get store data
|
|
local store = v.store
|
|
local inventory = store.inventory
|
|
|
|
-- Remove the original inventory array, we'll replace it with our summaries
|
|
store.inventory = nil
|
|
|
|
-- Create summary by category
|
|
local summary = {}
|
|
local transformed = {}
|
|
|
|
-- Group and analyze items by category
|
|
for _, item in ipairs(inventory) do
|
|
-- Prepare category data if not exists
|
|
local category = item.category
|
|
if not summary[category] then
|
|
summary[category] = {
|
|
count = 0,
|
|
total_value = 0,
|
|
low_stock_items = {}
|
|
}
|
|
end
|
|
|
|
-- Update category counts
|
|
summary[category].count = summary[category].count + 1
|
|
|
|
-- Calculate total value (price * stock) and add to category
|
|
local item_value = item.price * item.stock
|
|
summary[category].total_value = summary[category].total_value + item_value
|
|
|
|
-- Check for low stock (less than 10)
|
|
if item.stock < 10 then
|
|
table.insert(summary[category].low_stock_items, {
|
|
id = item.id,
|
|
name = item.name,
|
|
stock = item.stock
|
|
})
|
|
end
|
|
|
|
-- Create transformed view of the item with added tax
|
|
table.insert(transformed, {
|
|
name = item.name,
|
|
price_with_tax = math.floor((item.price * 1.2) * 100 + 0.5) / 100, -- 20% tax, rounded to 2 decimals
|
|
in_stock = item.stock > 0
|
|
})
|
|
end
|
|
|
|
-- Format the total_value with two decimal places
|
|
for category, data in pairs(summary) do
|
|
data.total_value = math.floor(data.total_value * 100 + 0.5) / 100
|
|
end
|
|
|
|
-- Add our new data structures to the store
|
|
store.inventory_summary = summary
|
|
store.transformed_items = transformed
|
|
`)
|
|
|
|
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)
|
|
}
|
|
|
|
// Parse both results as JSON objects for deep comparison
|
|
var resultObj map[string]interface{}
|
|
var expectedObj map[string]interface{}
|
|
|
|
if err := json.Unmarshal([]byte(result), &resultObj); err != nil {
|
|
t.Fatalf("Failed to parse result JSON: %v", err)
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
|
|
t.Fatalf("Failed to parse expected JSON: %v", err)
|
|
}
|
|
|
|
// Verify the structure and key counts
|
|
resultStore, ok := resultObj["store"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected 'store' object in result")
|
|
}
|
|
|
|
// Check that inventory is gone and replaced with our new structures
|
|
if resultStore["inventory"] != nil {
|
|
t.Errorf("Expected 'inventory' to be removed")
|
|
}
|
|
|
|
if resultStore["inventory_summary"] == nil {
|
|
t.Errorf("Expected 'inventory_summary' to be added")
|
|
}
|
|
|
|
if resultStore["transformed_items"] == nil {
|
|
t.Errorf("Expected 'transformed_items' to be added")
|
|
}
|
|
|
|
// Check that the transformed_items array has the correct length
|
|
transformedItems, ok := resultStore["transformed_items"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected 'transformed_items' to be an array")
|
|
}
|
|
|
|
if len(transformedItems) != 4 {
|
|
t.Errorf("Expected 'transformed_items' to have 4 items, got %d", len(transformedItems))
|
|
}
|
|
|
|
// Check that the summary has entries for both electronics and clothing
|
|
summary, ok := resultStore["inventory_summary"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected 'inventory_summary' to be an object")
|
|
}
|
|
|
|
if summary["electronics"] == nil {
|
|
t.Errorf("Expected 'electronics' category in summary")
|
|
}
|
|
|
|
if summary["clothing"] == nil {
|
|
t.Errorf("Expected 'clothing' category in summary")
|
|
}
|
|
}
|