diff --git a/processor/json.go b/processor/json.go deleted file mode 100644 index aff788b..0000000 --- a/processor/json.go +++ /dev/null @@ -1,194 +0,0 @@ -package processor - -import ( - "encoding/json" - "fmt" - "modify/logger" - "modify/processor/jsonpath" - - lua "github.com/yuin/gopher-lua" -) - -// JSONProcessor implements the Processor interface for JSON documents -type JSONProcessor struct{} - -// ProcessContent implements the Processor interface for JSONProcessor -func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) { - logger.Debug("Processing JSON content with JSONPath: %s", pattern) - - // Parse JSON document - logger.Trace("Parsing JSON document") - var jsonData interface{} - err := json.Unmarshal([]byte(content), &jsonData) - if err != nil { - logger.Error("Failed to parse JSON: %v", err) - return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err) - } - - // Find nodes matching the JSONPath pattern - logger.Debug("Executing JSONPath query: %s", pattern) - nodes, err := jsonpath.Get(jsonData, pattern) - if err != nil { - logger.Error("Failed to execute JSONPath: %v", err) - return content, 0, 0, fmt.Errorf("error getting nodes: %v", err) - } - - matchCount := len(nodes) - logger.Debug("Found %d nodes matching JSONPath", matchCount) - if matchCount == 0 { - logger.Warning("No nodes matched the JSONPath pattern: %s", pattern) - return content, 0, 0, nil - } - - modCount := 0 - for i, node := range nodes { - logger.Trace("Processing node #%d at path: %s with value: %v", i+1, node.Path, node.Value) - - // Initialize Lua - L, err := NewLuaState() - if err != nil { - logger.Error("Failed to create Lua state: %v", err) - return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err) - } - defer L.Close() - logger.Trace("Lua state initialized successfully") - - err = p.ToLua(L, node.Value) - if err != nil { - logger.Error("Failed to convert value to Lua: %v", err) - return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err) - } - logger.Trace("Converted node value to Lua: %v", node.Value) - - originalScript := luaExpr - fullScript := BuildLuaScript(luaExpr) - logger.Debug("Original script: %q, Full script: %q", originalScript, fullScript) - - // Execute Lua script - logger.Trace("Executing Lua script: %q", fullScript) - if err := L.DoString(fullScript); err != nil { - logger.Error("Failed to execute Lua script: %v", err) - return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err) - } - logger.Trace("Lua script executed successfully") - - // Get modified value - result, err := p.FromLua(L) - if err != nil { - logger.Error("Failed to get result from Lua: %v", err) - return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err) - } - logger.Trace("Retrieved modified value from Lua: %v", result) - - modified := false - modified = L.GetGlobal("modified").String() == "true" - if !modified { - logger.Debug("No changes made to node at path: %s", node.Path) - continue - } - - // Apply the modification to the JSON data - logger.Debug("Updating JSON at path: %s with new value: %v", node.Path, result) - err = p.updateJSONValue(jsonData, node.Path, result) - if err != nil { - logger.Error("Failed to update JSON at path %s: %v", node.Path, err) - return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err) - } - logger.Debug("Updated JSON at path: %s successfully", node.Path) - modCount++ - } - - logger.Info("JSON processing complete: %d modifications from %d matches", modCount, matchCount) - - // Convert the modified JSON back to a string with same formatting - logger.Trace("Marshalling JSON data back to string") - var jsonBytes []byte - jsonBytes, err = json.MarshalIndent(jsonData, "", " ") - if err != nil { - logger.Error("Failed to marshal JSON: %v", err) - return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err) - } - return string(jsonBytes), modCount, matchCount, nil -} - -// updateJSONValue updates a value in the JSON structure based on its JSONPath -func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error { - logger.Trace("Updating JSON value at path: %s", path) - - // Special handling for root node - if path == "$" { - logger.Debug("Handling special case for root node update") - // 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 - logger.Debug("Root was not a map, replacing entire root") - return jsonpath.Set(jsonData, path, newValue) - } - - // Clear the original map - logger.Trace("Clearing original root map") - for k := range rootMap { - delete(rootMap, k) - } - - // Copy all keys from the new map - logger.Trace("Copying keys to root 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 - logger.Debug("Root was not an array, replacing entire root") - return jsonpath.Set(jsonData, path, newValue) - } - - // Clear and recreate the array - logger.Trace("Replacing root array") - *&rootArray = rootValue - return nil - - default: - // For other types, use jsonpath.Set - logger.Debug("Replacing root with primitive value") - return jsonpath.Set(jsonData, path, newValue) - } - } - - // For non-root paths, use the regular Set method - logger.Trace("Using regular Set method for non-root path") - err := jsonpath.Set(jsonData, path, newValue) - if err != nil { - logger.Error("Failed to set JSON value at path %s: %v", path, err) - return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err) - } - return nil -} - -// ToLua converts JSON values to Lua variables -func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error { - table, err := ToLua(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 FromLua(L, luaValue) -} diff --git a/processor/json_test.go b/processor/json_test.go deleted file mode 100644 index 7070348..0000000 --- a/processor/json_test.go +++ /dev/null @@ -1,1771 +0,0 @@ -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") - } -} diff --git a/processor/xml.go b/processor/xml.go deleted file mode 100644 index 4943312..0000000 --- a/processor/xml.go +++ /dev/null @@ -1,434 +0,0 @@ -package processor - -import ( - "fmt" - "modify/logger" - "modify/processor/xpath" - "strings" - - "github.com/antchfx/xmlquery" - lua "github.com/yuin/gopher-lua" -) - -// XMLProcessor implements the Processor interface for XML documents -type XMLProcessor struct{} - -// ProcessContent implements the Processor interface for XMLProcessor -func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr string) (string, int, int, error) { - logger.Debug("Processing XML content with XPath: %s", path) - - // Parse XML document - // We can't really use encoding/xml here because it requires a pre defined struct - // And we HAVE TO parse dynamic unknown XML - logger.Trace("Parsing XML document") - doc, err := xmlquery.Parse(strings.NewReader(content)) - if err != nil { - logger.Error("Failed to parse XML: %v", err) - return content, 0, 0, fmt.Errorf("error parsing XML: %v", err) - } - - // Find nodes matching the XPath pattern - logger.Debug("Executing XPath query: %s", path) - nodes, err := xpath.Get(doc, path) - if err != nil { - logger.Error("Failed to execute XPath: %v", err) - return content, 0, 0, fmt.Errorf("error executing XPath: %v", err) - } - - matchCount := len(nodes) - logger.Debug("Found %d nodes matching XPath", matchCount) - if matchCount == 0 { - logger.Warning("No nodes matched the XPath pattern: %s", path) - return content, 0, 0, nil - } - - // Apply modifications to each node - modCount := 0 - for i, node := range nodes { - logger.Trace("Processing node #%d: %s", i+1, node.Data) - - L, err := NewLuaState() - if err != nil { - logger.Error("Failed to create Lua state: %v", err) - return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err) - } - defer L.Close() - - logger.Trace("Converting XML node to Lua") - err = p.ToLua(L, node) - if err != nil { - logger.Error("Failed to convert XML node to Lua: %v", err) - return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err) - } - - luaScript := BuildLuaScript(luaExpr) - logger.Trace("Executing Lua script: %s", luaScript) - err = L.DoString(luaScript) - if err != nil { - logger.Error("Failed to execute Lua script: %v", err) - return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err) - } - - result, err := p.FromLua(L) - if err != nil { - logger.Error("Failed to get result from Lua: %v", err) - return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err) - } - logger.Trace("Lua returned result: %#v", result) - - modified := false - modified = L.GetGlobal("modified").String() == "true" - if !modified { - logger.Debug("No changes made to node at path: %s", node.Data) - continue - } - - // Apply modification based on the result - if updatedValue, ok := result.(string); ok { - // If the result is a simple string, update the node value directly - logger.Debug("Updating node with string value: %s", updatedValue) - xpath.Set(doc, path, updatedValue) - } else if nodeData, ok := result.(map[string]interface{}); ok { - // If the result is a map, apply more complex updates - logger.Debug("Updating node with complex data structure") - updateNodeFromMap(node, nodeData) - } - - modCount++ - logger.Debug("Successfully modified node #%d", i+1) - } - - logger.Info("XML processing complete: %d modifications from %d matches", modCount, matchCount) - - // Serialize the modified XML document to string - if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode { - // If we have an XML declaration, start with it - declaration := doc.FirstChild.OutputXML(true) - // Remove the firstChild (declaration) before serializing the rest of the document - doc.FirstChild = doc.FirstChild.NextSibling - return ConvertToNamedEntities(declaration + doc.OutputXML(true)), modCount, matchCount, nil - } - - // Convert numeric entities to named entities for better readability - return ConvertToNamedEntities(doc.OutputXML(true)), modCount, matchCount, nil -} - -func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error { - table, err := p.ToLuaTable(L, data) - if err != nil { - return err - } - L.SetGlobal("v", table) - return nil -} - -// ToLua converts XML node values to Lua variables -func (p *XMLProcessor) ToLuaTable(L *lua.LState, data interface{}) (lua.LValue, error) { - // Check if data is an xmlquery.Node - node, ok := data.(*xmlquery.Node) - if !ok { - return nil, fmt.Errorf("expected xmlquery.Node, got %T", data) - } - - // Create a simple table with essential data - table := L.NewTable() - - // For element nodes, just provide basic info - L.SetField(table, "type", lua.LString(nodeTypeToString(node.Type))) - L.SetField(table, "name", lua.LString(node.Data)) - L.SetField(table, "value", lua.LString(node.InnerText())) - - // Add children if any - children := L.NewTable() - for child := node.FirstChild; child != nil; child = child.NextSibling { - childTable, err := p.ToLuaTable(L, child) - if err == nil { - children.Append(childTable) - } - } - L.SetField(table, "children", children) - - attrs := L.NewTable() - if len(node.Attr) > 0 { - for _, attr := range node.Attr { - L.SetField(attrs, attr.Name.Local, lua.LString(attr.Value)) - } - } - L.SetField(table, "attr", attrs) - - return table, nil -} - -// FromLua gets modified values from Lua -func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) { - luaValue := L.GetGlobal("v") - - // Handle string values directly - if luaValue.Type() == lua.LTString { - return luaValue.String(), nil - } - - // Handle tables (for attributes and more complex updates) - if luaValue.Type() == lua.LTTable { - return luaTableToMap(L, luaValue.(*lua.LTable)), nil - } - - return luaValue.String(), nil -} - -// Simple helper to convert a Lua table to a Go map -func luaTableToMap(L *lua.LState, table *lua.LTable) map[string]interface{} { - result := make(map[string]interface{}) - - table.ForEach(func(k, v lua.LValue) { - if k.Type() == lua.LTString { - key := k.String() - - if v.Type() == lua.LTTable { - result[key] = luaTableToMap(L, v.(*lua.LTable)) - } else { - result[key] = v.String() - } - } - }) - - return result -} - -// Simple helper to convert node type to string -func nodeTypeToString(nodeType xmlquery.NodeType) string { - switch nodeType { - case xmlquery.ElementNode: - return "element" - case xmlquery.TextNode: - return "text" - case xmlquery.AttributeNode: - return "attribute" - default: - return "other" - } -} - -// Helper function to update an XML node from a map -func updateNodeFromMap(node *xmlquery.Node, data map[string]interface{}) { - // Update node value if present - if value, ok := data["value"]; ok { - if strValue, ok := value.(string); ok { - // For element nodes, replace text content - if node.Type == xmlquery.ElementNode { - // Find the first text child if it exists - var textNode *xmlquery.Node - for child := node.FirstChild; child != nil; child = child.NextSibling { - if child.Type == xmlquery.TextNode { - textNode = child - break - } - } - - if textNode != nil { - // Update existing text node - textNode.Data = strValue - } else { - // Create new text node - newText := &xmlquery.Node{ - Type: xmlquery.TextNode, - Data: strValue, - Parent: node, - } - - // Insert at beginning of children - if node.FirstChild != nil { - newText.NextSibling = node.FirstChild - node.FirstChild.PrevSibling = newText - node.FirstChild = newText - } else { - node.FirstChild = newText - node.LastChild = newText - } - } - } else if node.Type == xmlquery.TextNode { - // Directly update text node - node.Data = strValue - } else if node.Type == xmlquery.AttributeNode { - // Update attribute value - if node.Parent != nil { - for i, attr := range node.Parent.Attr { - if attr.Name.Local == node.Data { - node.Parent.Attr[i].Value = strValue - break - } - } - } - } - } - } - - // Update attributes if present - if attrs, ok := data["attr"].(map[string]interface{}); ok && node.Type == xmlquery.ElementNode { - for name, value := range attrs { - if strValue, ok := value.(string); ok { - // Look for existing attribute - found := false - for i, attr := range node.Attr { - if attr.Name.Local == name { - node.Attr[i].Value = strValue - found = true - break - } - } - - // Add new attribute if not found - if !found { - node.Attr = append(node.Attr, xmlquery.Attr{ - Name: struct { - Space, Local string - }{Local: name}, - Value: strValue, - }) - } - } - } - } -} - -// Helper function to get a string representation of node type -func nodeTypeName(nodeType xmlquery.NodeType) string { - switch nodeType { - case xmlquery.ElementNode: - return "element" - case xmlquery.TextNode: - return "text" - case xmlquery.AttributeNode: - return "attribute" - case xmlquery.CommentNode: - return "comment" - case xmlquery.DeclarationNode: - return "declaration" - default: - return "unknown" - } -} - -// ConvertToNamedEntities replaces numeric XML entities with their named counterparts -func ConvertToNamedEntities(xml string) string { - // Basic XML entities - replacements := map[string]string{ - // Basic XML entities - """: """, // double quote - "'": "'", // single quote - "<": "<", // less than - ">": ">", // greater than - "&": "&", // ampersand - - // Common symbols - " ": " ", // non-breaking space - "©": "©", // copyright - "®": "®", // registered trademark - "€": "€", // euro - "£": "£", // pound - "¥": "¥", // yen - "¢": "¢", // cent - "§": "§", // section - "™": "™", // trademark - "♠": "♠", // spade - "♣": "♣", // club - "♥": "♥", // heart - "♦": "♦", // diamond - - // Special characters - "¡": "¡", // inverted exclamation - "¿": "¿", // inverted question - "«": "«", // left angle quotes - "»": "»", // right angle quotes - "·": "·", // middle dot - "•": "•", // bullet - "…": "…", // horizontal ellipsis - "′": "′", // prime - "″": "″", // double prime - "‾": "‾", // overline - "⁄": "⁄", // fraction slash - - // Math symbols - "±": "±", // plus-minus - "×": "×", // multiplication - "÷": "÷", // division - "∞": "∞", // infinity - "≈": "≈", // almost equal - "≠": "≠", // not equal - "≤": "≤", // less than or equal - "≥": "≥", // greater than or equal - "∑": "∑", // summation - "√": "√", // square root - "∫": "∫", // integral - - // Accented characters - "À": "À", // A grave - "Á": "Á", // A acute - "Â": "Â", // A circumflex - "Ã": "Ã", // A tilde - "Ä": "Ä", // A umlaut - "Å": "Å", // A ring - "Æ": "Æ", // AE ligature - "Ç": "Ç", // C cedilla - "È": "È", // E grave - "É": "É", // E acute - "Ê": "Ê", // E circumflex - "Ë": "Ë", // E umlaut - "Ì": "Ì", // I grave - "Í": "Í", // I acute - "Î": "Î", // I circumflex - "Ï": "Ï", // I umlaut - "Ð": "Ð", // Eth - "Ñ": "Ñ", // N tilde - "Ò": "Ò", // O grave - "Ó": "Ó", // O acute - "Ô": "Ô", // O circumflex - "Õ": "Õ", // O tilde - "Ö": "Ö", // O umlaut - "Ø": "Ø", // O slash - "Ù": "Ù", // U grave - "Ú": "Ú", // U acute - "Û": "Û", // U circumflex - "Ü": "Ü", // U umlaut - "Ý": "Ý", // Y acute - "Þ": "Þ", // Thorn - "ß": "ß", // Sharp s - "à": "à", // a grave - "á": "á", // a acute - "â": "â", // a circumflex - "ã": "ã", // a tilde - "ä": "ä", // a umlaut - "å": "å", // a ring - "æ": "æ", // ae ligature - "ç": "ç", // c cedilla - "è": "è", // e grave - "é": "é", // e acute - "ê": "ê", // e circumflex - "ë": "ë", // e umlaut - "ì": "ì", // i grave - "í": "í", // i acute - "î": "î", // i circumflex - "ï": "ï", // i umlaut - "ð": "ð", // eth - "ñ": "ñ", // n tilde - "ò": "ò", // o grave - "ó": "ó", // o acute - "ô": "ô", // o circumflex - "õ": "õ", // o tilde - "ö": "ö", // o umlaut - "ø": "ø", // o slash - "ù": "ù", // u grave - "ú": "ú", // u acute - "û": "û", // u circumflex - "ü": "ü", // u umlaut - "ý": "ý", // y acute - "þ": "þ", // thorn - "ÿ": "ÿ", // y umlaut - } - - result := xml - for numeric, named := range replacements { - result = strings.ReplaceAll(result, numeric, named) - } - return result -} diff --git a/processor/xml_test.go b/processor/xml_test.go deleted file mode 100644 index cda1ce5..0000000 --- a/processor/xml_test.go +++ /dev/null @@ -1,1772 +0,0 @@ -package processor - -import ( - "strings" - "testing" - - "regexp" - ) - -// Helper function to normalize whitespace for comparison -func normalizeXMLWhitespace(s string) string { - // Replace all whitespace sequences with a single space - re := regexp.MustCompile(`\s+`) - s = re.ReplaceAllString(strings.TrimSpace(s), " ") - - // Normalize XML entities for comparison - s = ConvertToNamedEntities(s) - - return s -} - -func TestXMLProcessor_Process_NodeValues(t *testing.T) { - content := ` - - - Gambardella, Matthew - XML Developer's Guide - Computer - 44.95 - 2000-10-01 - An in-depth look at creating applications with XML. - - - Ralls, Kim - Midnight Rain - Fantasy - 5.95 - 2000-12-16 - A former architect battles corporate zombies. - -` - - expected := ` - - - Gambardella, Matthew - XML Developer's Guide - Computer - 89.9 - 2000-10-01 - An in-depth look at creating applications with XML. - - - Ralls, Kim - Midnight Rain - Fantasy - 11.9 - 2000-12-16 - A former architect battles corporate zombies. - -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v.value = v.value * 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_Attributes(t *testing.T) { - content := ` - - Widget A - Widget B -` - - expected := ` - - Widget A - Widget B -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//item/@price", "v.value = v.value * 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_ElementText(t *testing.T) { - content := ` - - john - mary -` - - expected := ` - - JOHN - MARY -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//n/text()", "v.value = string.upper(v.value)") - - 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_ElementAddition(t *testing.T) { - content := ` - - - 30 - 100 - -` - - expected := ` - - - 60 - 200 - -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//settings/*", "v.value = v.value * 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_ComplexXML(t *testing.T) { - content := ` - - - - Laptop - 999.99 - - - Smartphone - 499.99 - - - - - T-Shirt - 19.99 - - -` - - expected := ` - - - - Laptop - 1199.988 - - - Smartphone - 599.988 - - - - - T-Shirt - 23.988 - - -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v.value = round(v.value * 1.2, 3)") - - 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -// New tests added below - -func TestXMLProcessor_ConditionalModification(t *testing.T) { - content := ` - - - - -` - - expected := ` - - - - -` - - p := &XMLProcessor{} - // Apply 20% discount but only for items with stock > 0 - luaExpr := ` - -- In the table-based approach, attributes are accessible directly - if v.attr.stock and tonumber(v.attr.stock) > 0 then - v.attr.price = tonumber(v.attr.price) * 0.8 - -- Format to 2 decimal places - v.attr.price = string.format("%.2f", v.attr.price) - else - return false - end - ` - result, modCount, matchCount, err := p.ProcessContent(content, "//item", luaExpr) - - 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_SpecialCharacters(t *testing.T) { - content := ` - - This & that - a < b - c > d - Quote: "Hello" -` - - expected := ` - - THIS & THAT - A < B - C > D - QUOTE: "HELLO" -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//entry", "v.value = string.upper(v.value)") - - 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) - } - - // Normalize whitespace for comparison - normalizedResult := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_ChainedOperations(t *testing.T) { - content := ` - - - Widget - 100 - 20 - -` - - // Apply multiple operations to the price: add tax, apply discount, round - luaExpr := ` - local price = v.value - -- Add 15% tax - price = price * 1.15 - -- Apply 10% discount - price = price * 0.9 - -- Round to 2 decimal places - price = round(price, 2) - v.value = price - ` - - expected := ` - - - Widget - 103.5 - 20 - -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//price", luaExpr) - - 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_MathFunctions(t *testing.T) { - content := ` - - 3.14159 - 2.71828 - 1.41421 -` - - expected := ` - - 3 - 3 - 1 -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//measurement", "v.value = round(v.value)") - - 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_StringOperations(t *testing.T) { - content := ` - - - John Doe - john.doe@example.com - 123-456-7890 - -` - - expected := ` - - - John Doe - johndoe@anon.com - 123-XXX-XXXX - -` - - // Test email anonymization - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//email", ` - -- With the table approach, v contains the text content directly - v.value = string.gsub(v.value, "@.+", "@anon.com") - local username = string.match(v.value, "(.+)@") - v.value = string.gsub(username, "%.", "") .. "@anon.com" - `) - - if err != nil { - t.Fatalf("Error processing email content: %v", err) - } - - // Test phone number masking - result, modCount2, matchCount2, err := p.ProcessContent(result, "//phone", ` - v.value = string.gsub(v.value, "%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 += matchCount2 - modCount += modCount2 - - if matchCount != 2 { - t.Errorf("Expected 2 total matches, got %d", matchCount) - } - - if modCount != 2 { - t.Errorf("Expected 2 total modifications, got %d", modCount) - } - - // Normalize whitespace for comparison - normalizedResult := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_DateManipulation(t *testing.T) { - content := ` - - - Conference - 2023-06-15 - - - Workshop - 2023-06-20 - -` - - expected := ` - - - Conference - 2023-07-15 - - - Workshop - 2023-07-20 - -` - - p := &XMLProcessor{} - result, modCount, matchCount, err := p.ProcessContent(content, "//date", ` - local year, month, day = string.match(v.value, "(%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.value = 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) - } - - // Normalize whitespace for comparison - normalizedResult := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_Error_InvalidXML(t *testing.T) { - content := ` - - -` - - p := &XMLProcessor{} - _, _, _, err := p.ProcessContent(content, "//unclosed", "v1=v1") - - if err == nil { - t.Errorf("Expected an error for invalid XML, but got none") - } -} - -func TestXMLProcessor_Process_Error_InvalidXPath(t *testing.T) { - content := ` - - value -` - - p := &XMLProcessor{} - _, _, _, err := p.ProcessContent(content, "[invalid xpath]", "v1=v1") - - if err == nil { - t.Errorf("Expected an error for invalid XPath, but got none") - } -} - -func TestXMLProcessor_Process_Error_InvalidLua(t *testing.T) { - content := ` - - 123 -` - - p := &XMLProcessor{} - _, _, _, err := p.ProcessContent(content, "//element", "v1 = invalid_function()") - - if err == nil { - t.Errorf("Expected an error for invalid Lua, but got none") - } -} - -func TestXMLProcessor_Process_ComplexXPathSelectors(t *testing.T) { - content := ` - - - - The Imaginary World - Alice Johnson - 19.99 - - - History of Science - Bob Smith - 29.99 - - - Future Tales - Charlie Adams - 24.99 - - -` - - expected := ` - - - - The Imaginary World - Alice Johnson - 15.99 - - - History of Science - Bob Smith - 29.99 - - - Future Tales - Charlie Adams - 19.99 - - -` - - p := &XMLProcessor{} - // Target only fiction books and apply 20% discount to price - result, modCount, matchCount, err := p.ProcessContent(content, "//book[@category='fiction']/price", "v.value = round(v.value * 0.8, 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -func TestXMLProcessor_Process_NestedStructureModification(t *testing.T) { - content := ` - - - - - 10 - 15 - 12 - - - Sword - Leather - - - - - 14 - 10 - 16 - - - Axe - Chain Mail - - - -` - - expected := ` - - - - - 12 - 18 - 14 - - - Sword - Leather - - - - - 14 - 10 - 16 - - - Axe - Chain Mail - - - -` - - p := &XMLProcessor{} - - // Boost hero stats by 20% - result, modCount, matchCount, err := p.ProcessContent(content, "//character[@id='hero']/stats/*", "v.value = round(v.value * 1.2)") - if err != nil { - t.Fatalf("Error processing stats content: %v", err) - } - - // Also upgrade hero equipment - result, modCount2, matchCount2, err := p.ProcessContent(result, "//character[@id='hero']/equipment/*/@damage|//character[@id='hero']/equipment/*/@defense", "v.value = v.value + 2") - if err != nil { - t.Fatalf("Error processing equipment content: %v", err) - } - - totalMatches := matchCount + matchCount2 - totalMods := modCount + modCount2 - - if totalMatches != 5 { // 3 stats + 2 equipment attributes - t.Errorf("Expected 5 total matches, got %d", totalMatches) - } - - if totalMods != 5 { // 3 stats + 2 equipment attributes - t.Errorf("Expected 5 total modifications, got %d", totalMods) - } - - // Normalize whitespace for comparison - normalizedResult := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -// func TestXMLProcessor_Process_ElementReplacement(t *testing.T) { -// content := ` -// -// -// Apple -// 1.99 -// 10 -// -// -// Carrot -// 0.99 -// 5 -// -// ` -// -// expected := ` -// -// -// Apple -// 1.99 -// 10 -// 19.90 -// -// -// Carrot -// 0.99 -// 5 -// 4.95 -// -// ` -// -// // This test demonstrates using variables from multiple elements to calculate a new value -// // With the table approach, we can directly access child elements -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- With a proper table approach, this becomes much simpler -// local price = tonumber(v.attr.price) -// local quantity = tonumber(v.attr.quantity) -// -// -- Add a new total element -// v.total = string.format("%.2f", price * quantity) -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "//item", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_AttributeAddition(t *testing.T) { -// content := ` -// -// -// Laptop -// 999.99 -// true -// -// -// Phone -// 499.99 -// false -// -// ` -// -// expected := ` -// -// -// Laptop -// 999.99 -// true -// -// -// Phone -// 499.99 -// false -// -// ` -// -// // This test demonstrates adding a new attribute based on element content -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- With table approach, this becomes much cleaner -// -- We can access the "inStock" element directly -// if v.inStock == "true" then -// -- Add a new attribute directly -// v.attr = v.attr or {} -// v.attr.status = "available" -// else -// v.attr = v.attr or {} -// v.attr.status = "out-of-stock" -// end -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "//product", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_ElementRemoval(t *testing.T) { -// content := ` -// -// -// John Smith -// john@example.com -// secret123 -// admin -// -// -// Jane Doe -// jane@example.com -// pass456 -// user -// -// ` -// -// expected := ` -// -// -// John Smith -// john@example.com -// admin -// -// -// Jane Doe -// jane@example.com -// user -// -// ` -// -// // This test demonstrates removing sensitive data elements -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- With table approach, element removal is trivial -// -- Just set the element to nil to remove it -// v.password = nil -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "//user", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_ElementReordering(t *testing.T) { -// content := ` -// -// -// Bob Dylan -// Blowin' in the Wind -// 1963 -// -// -// The Beatles -// Hey Jude -// 1968 -// -// ` -// -// expected := ` -// -// -// Blowin' in the Wind -// Bob Dylan -// 1963 -// -// -// Hey Jude -// The Beatles -// 1968 -// -// ` -// -// // This test demonstrates reordering elements -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- With table approach, we can reorder elements by redefining the table -// -- Store the values -// local artist = v.attr.artist -// local title = v.attr.title -// local year = v.attr.year -// -// -- Clear the table -// for k in pairs(v) do -// v[k] = nil -// end -// -// -- Add elements in the desired order -// v.attr.title = title -// v.attr.artist = artist -// v.attr.year = year -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "//song", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_ComplexStructuralChange(t *testing.T) { -// content := ` -// -// -// The Great Gatsby -// F. Scott Fitzgerald -// 1925 -// 10.99 -// -// -// A Brief History of Time -// Stephen Hawking -// 1988 -// 15.99 -// -// ` -// -// expected := ` -// -// -//
-// The Great Gatsby -// F. Scott Fitzgerald -// 1925 -//
-// -// 10.99 -// 0 -// -// -// fiction -// -//
-// -//
-// A Brief History of Time -// Stephen Hawking -// 1988 -//
-// -// 15.99 -// 0 -// -// -// non-fiction -// -//
-//
` -// -// // This test demonstrates a complete restructuring of the XML using table approach -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- Store the original values -// local category = v._attr and v._attr.category -// local title = v.title -// local author = v.author -// local year = v.year -// local price = v.price -// -// -- Clear the original structure -// for k in pairs(v) do -// v[k] = nil -// end -// -// -- Create a new nested structure -// v.details = { -// title = title, -// author = author, -// year = year -// } -// -// v.pricing = { -// price = { -// _attr = { currency = "USD" }, -// _text = price -// }, -// discount = "0" -// } -// -// v.metadata = { -// category = category -// } -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "//book", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -func TestXMLProcessor_Process_DynamicXPath(t *testing.T) { - content := ` - - - - - - - - - - -` - - expected := ` - - - - - - - - - - -` - - // This test demonstrates using specific XPath queries to select precise nodes - p := &XMLProcessor{} - - // Double all timeout values in the configuration - result, modCount, matchCount, err := p.ProcessContent(content, "//setting[@name='timeout']/@value", "v.value = v.value * 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 := normalizeXMLWhitespace(result) - normalizedExpected := normalizeXMLWhitespace(expected) - - if normalizedResult != normalizedExpected { - t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) - } -} - -// func TestXMLProcessor_Process_TableBasedStructureCreation(t *testing.T) { -// content := ` -// -// -// -// ` -// -// expected := ` -// -// -// -// -// -// 2 -// Debug: OFF, Logging: info -// -// -// ` -// -// // This test demonstrates adding a completely new section with nested structure -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- Count all options -// local count = 0 -// local summary = "" -// -// -- Process each child option -// local settings = v.children[1] -// local options = settings.children -// -- if settings and options then -// -- if options.attr then -// -- options = {options} -// -- end -// -- -// -- for i, opt in ipairs(options) do -// -- count = count + 1 -// -- if opt.attr.name == "debug" then -// -- summary = summary .. "Debug: " .. (opt.attr.value == "true" and "ON" or "OFF") -// -- elseif opt.attr.name == "log_level" then -// -- summary = summary .. "Logging: " .. opt.attr.value -// -- end -// -- -// -- if i < #options then -// -- summary = summary .. ", " -// -- end -// -- end -// -- end -// -// -- Create a new calculated section -// -- v.children[2] = { -// -- stats = { -// -- count = tostring(count), -// -- summary = summary -// -- } -// -- } -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "/data", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_ArrayManipulation(t *testing.T) { -// content := ` -// -// -// -// Book 1 -// 200 -// -// -// Book 2 -// 150 -// -// -// Book 3 -// 300 -// -// -// ` -// -// expected := ` -// -// -// -// Book 3 -// 300 -// -// -// Book 1 -// 200 -// -// -// -// 2 -// 500 -// 250 -// -// ` -// -// // This test demonstrates advanced manipulation including: -// // 1. Sorting and filtering arrays of elements -// // 2. Calculating aggregates -// // 3. Generating summaries -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- Get the books array -// local books = v.books.book -// -// -- If only one book, wrap it in a table -// if books and not books[1] then -// books = {books} -// end -// -// -- Filter and sort books -// local filtered_books = {} -// local total_pages = 0 -// -// for _, book in ipairs(books) do -// local pages = tonumber(book.pages) or 0 -// -// -- Filter: only keep books with pages >= 200 -// if pages >= 200 then -// total_pages = total_pages + pages -// table.insert(filtered_books, book) -// end -// end -// -// -- Sort books by number of pages (descending) -// table.sort(filtered_books, function(a, b) -// return tonumber(a.pages) > tonumber(b.pages) -// end) -// -// -- Replace the books array with our filtered and sorted one -// v.books.book = filtered_books -// -// -- Add summary information -// local count = #filtered_books -// local average_pages = count > 0 and math.floor(total_pages / count) or 0 -// -// v.summary = { -// count = tostring(count), -// total_pages = tostring(total_pages), -// average_pages = tostring(average_pages) -// } -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "/library", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// func TestXMLProcessor_Process_DeepPathNavigation(t *testing.T) { -// content := ` -// -// -// -// -// localhost -// 3306 -// -// admin -// secret -// -// -// -// 10 -// 30 -// -// -// -// info -// /var/log/app.log -// -// -// ` -// -// expected := ` -// -// -// -// -// db.example.com -// 5432 -// -// admin -// REDACTED -// -// -// -// 20 -// 60 -// -// -// -// debug -// /var/log/app.log -// -// -// -// production -// true -// true -// -// ` -// -// // This test demonstrates navigating deeply nested elements in a complex XML structure -// p := &XMLProcessor{} -// -// luaExpr := ` -// -- Update database connection settings -// v.config.database.connection.host = "db.example.com" -// v.config.database.connection.port = "5432" -// -// -- Redact sensitive information -// v.config.database.connection.credentials.password = "REDACTED" -// -// -- Double pool size and timeout -// v.config.database.pool.size = tostring(tonumber(v.config.database.pool.size) * 2) -// v.config.database.pool.timeout = tostring(tonumber(v.config.database.pool.timeout) * 2) -// -// -- Change logging level -// v.config.logging.level = "debug" -// -// -- Add a new status section -// v.status = { -// environment = "production", -// updated = "true", -// secure = tostring(v.config.database.connection.credentials.password == "REDACTED") -// } -// ` -// -// result, modCount, matchCount, err := p.ProcessContent(content, "/application", luaExpr) -// -// 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 := normalizeXMLWhitespace(result) -// normalizedExpected := normalizeXMLWhitespace(expected) -// -// if normalizedResult != normalizedExpected { -// t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) -// } -// } - -// Add more test cases for specific XML manipulation scenarios below -// These tests would cover additional functionality as the implementation progresses - -// func TestXMLToLua(t *testing.T) { -// // Sample XML to test with -// xmlStr := ` -// -// -//
-// 123 Main St -// Anytown -// 12345 -//
-// john@example.com -//
-// -//
-// 456 Business Ave -// Worktown -// 54321 -//
-// 555-1234 -//
-//
-// ` -// -// // Parse the XML -// doc, err := xmlquery.Parse(strings.NewReader(xmlStr)) -// if err != nil { -// t.Fatalf("Failed to parse XML: %v", err) -// } -// -// // Create a new Lua state -// L := lua.NewState() -// defer L.Close() -// -// // Create an XML processor -// processor := &XMLProcessor{} -// -// // Test converting the root element to Lua -// t.Run("RootElement", func(t *testing.T) { -// // Find the root element -// root := doc.SelectElement("root") -// if root == nil { -// t.Fatal("Failed to find root element") -// } -// -// // Convert to Lua -// err := processor.ToLua(L, root) -// if err != nil { -// t.Fatalf("Failed to convert to Lua: %v", err) -// } -// -// // Verify the result -// luaTable := L.GetGlobal("v") -// if luaTable.Type() != lua.LTTable { -// t.Fatalf("Expected table, got %s", luaTable.Type().String()) -// } -// -// // Check element type -// typeVal := L.GetField(luaTable, "type") -// if typeVal.String() != "element" { -// t.Errorf("Expected type 'element', got '%s'", typeVal.String()) -// } -// -// // Check name -// nameVal := L.GetField(luaTable, "name") -// if nameVal.String() != "root" { -// t.Errorf("Expected name 'root', got '%s'", nameVal.String()) -// } -// -// // Check attributes -// attrsTable := L.GetField(luaTable, "attributes") -// if attrsTable.Type() != lua.LTTable { -// t.Fatalf("Expected attributes table, got %s", attrsTable.Type().String()) -// } -// -// idVal := L.GetField(attrsTable, "id") -// if idVal.String() != "1" { -// t.Errorf("Expected id '1', got '%s'", idVal.String()) -// } -// -// // Check that we have children -// childrenTable := L.GetField(luaTable, "children") -// if childrenTable.Type() != lua.LTTable { -// t.Fatalf("Expected children table, got %s", childrenTable.Type().String()) -// } -// }) -// -// // Test converting a nested element to Lua -// t.Run("NestedElement", func(t *testing.T) { -// // Find a nested element -// street := doc.SelectElement("//street") -// if street == nil { -// t.Fatal("Failed to find street element") -// } -// -// // Convert to Lua -// err := processor.ToLua(L, street) -// if err != nil { -// t.Fatalf("Failed to convert to Lua: %v", err) -// } -// -// // Verify the result -// luaTable := L.GetGlobal("v") -// if luaTable.Type() != lua.LTTable { -// t.Fatalf("Expected table, got %s", luaTable.Type().String()) -// } -// -// // Check element type -// typeVal := L.GetField(luaTable, "type") -// if typeVal.String() != "element" { -// t.Errorf("Expected type 'element', got '%s'", typeVal.String()) -// } -// -// // Check name -// nameVal := L.GetField(luaTable, "name") -// if nameVal.String() != "street" { -// t.Errorf("Expected name 'street', got '%s'", nameVal.String()) -// } -// -// // Check value -// valueVal := L.GetField(luaTable, "value") -// if valueVal.String() != "123 Main St" { -// t.Errorf("Expected value '123 Main St', got '%s'", valueVal.String()) -// } -// }) -// -// // Test FromLua with a simple string update -// t.Run("FromLuaString", func(t *testing.T) { -// // Set up a Lua state with a string value -// L := lua.NewState() -// defer L.Close() -// L.SetGlobal("v", lua.LString("New Value")) -// -// // Convert from Lua -// result, err := processor.FromLua(L) -// if err != nil { -// t.Fatalf("Failed to convert from Lua: %v", err) -// } -// -// // Verify the result -// strResult, ok := result.(string) -// if !ok { -// t.Fatalf("Expected string result, got %T", result) -// } -// -// if strResult != "New Value" { -// t.Errorf("Expected 'New Value', got '%s'", strResult) -// } -// }) -// -// // Test FromLua with a complex table update -// t.Run("FromLuaTable", func(t *testing.T) { -// // Set up a Lua state with a table value -// L := lua.NewState() -// defer L.Close() -// -// table := L.NewTable() -// L.SetField(table, "value", lua.LString("Updated Text")) -// -// attrTable := L.NewTable() -// L.SetField(attrTable, "id", lua.LString("new-id")) -// L.SetField(attrTable, "class", lua.LString("highlight")) -// -// L.SetField(table, "attributes", attrTable) -// L.SetGlobal("v", table) -// -// // Convert from Lua -// result, err := processor.FromLua(L) -// if err != nil { -// t.Fatalf("Failed to convert from Lua: %v", err) -// } -// -// // Verify the result -// mapResult, ok := result.(map[string]interface{}) -// if !ok { -// t.Fatalf("Expected map result, got %T", result) -// } -// -// // Check value -// if value, ok := mapResult["value"]; !ok || value != "Updated Text" { -// t.Errorf("Expected value 'Updated Text', got '%v'", value) -// } -// -// // Check attributes -// attrs, ok := mapResult["attributes"].(map[string]interface{}) -// if !ok { -// t.Fatalf("Expected attributes map, got %T", mapResult["attributes"]) -// } -// -// if id, ok := attrs["id"]; !ok || id != "new-id" { -// t.Errorf("Expected id 'new-id', got '%v'", id) -// } -// -// if class, ok := attrs["class"]; !ok || class != "highlight" { -// t.Errorf("Expected class 'highlight', got '%v'", class) -// } -// }) -// -// // Test updateNodeFromMap with a simple value update -// t.Run("UpdateNodeValue", func(t *testing.T) { -// // Create a simple element to update -// xmlStr := `Original Text` -// doc, _ := xmlquery.Parse(strings.NewReader(xmlStr)) -// node := doc.SelectElement("test") -// -// // Create update data -// updateData := map[string]interface{}{ -// "value": "Updated Text", -// } -// -// // Update the node -// updateNodeFromMap(node, updateData) -// -// // Verify the update -// if node.InnerText() != "Updated Text" { -// t.Errorf("Expected value 'Updated Text', got '%s'", node.InnerText()) -// } -// }) -// -// // Test updateNodeFromMap with attribute updates -// t.Run("UpdateNodeAttributes", func(t *testing.T) { -// // Create an element with attributes -// xmlStr := `Text` -// doc, _ := xmlquery.Parse(strings.NewReader(xmlStr)) -// node := doc.SelectElement("test") -// -// // Create update data -// updateData := map[string]interface{}{ -// "attributes": map[string]interface{}{ -// "id": "new", -// "class": "added", -// }, -// } -// -// // Update the node -// updateNodeFromMap(node, updateData) -// -// // Verify the id attribute was updated -// idFound := false -// classFound := false -// for _, attr := range node.Attr { -// if attr.Name.Local == "id" { -// idFound = true -// if attr.Value != "new" { -// t.Errorf("Expected id 'new', got '%s'", attr.Value) -// } -// } -// if attr.Name.Local == "class" { -// classFound = true -// if attr.Value != "added" { -// t.Errorf("Expected class 'added', got '%s'", attr.Value) -// } -// } -// } -// -// if !idFound { -// t.Error("Expected to find 'id' attribute but didn't") -// } -// -// if !classFound { -// t.Error("Expected to find 'class' attribute but didn't") -// } -// }) -// }