1778 lines
44 KiB
Go
1778 lines
44 KiB
Go
package processor
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"regexp"
|
|
|
|
"github.com/antchfx/xmlquery"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<catalog>
|
|
<book id="bk101">
|
|
<author>Gambardella, Matthew</author>
|
|
<title>XML Developer's Guide</title>
|
|
<genre>Computer</genre>
|
|
<price>44.95</price>
|
|
<publish_date>2000-10-01</publish_date>
|
|
<description>An in-depth look at creating applications with XML.</description>
|
|
</book>
|
|
<book id="bk102">
|
|
<author>Ralls, Kim</author>
|
|
<title>Midnight Rain</title>
|
|
<genre>Fantasy</genre>
|
|
<price>5.95</price>
|
|
<publish_date>2000-12-16</publish_date>
|
|
<description>A former architect battles corporate zombies.</description>
|
|
</book>
|
|
</catalog>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<catalog>
|
|
<book id="bk101">
|
|
<author>Gambardella, Matthew</author>
|
|
<title>XML Developer's Guide</title>
|
|
<genre>Computer</genre>
|
|
<price>89.9</price>
|
|
<publish_date>2000-10-01</publish_date>
|
|
<description>An in-depth look at creating applications with XML.</description>
|
|
</book>
|
|
<book id="bk102">
|
|
<author>Ralls, Kim</author>
|
|
<title>Midnight Rain</title>
|
|
<genre>Fantasy</genre>
|
|
<price>11.9</price>
|
|
<publish_date>2000-12-16</publish_date>
|
|
<description>A former architect battles corporate zombies.</description>
|
|
</book>
|
|
</catalog>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<items>
|
|
<item price="10.50" quantity="1">Widget A</item>
|
|
<item price="5.25" quantity="3">Widget B</item>
|
|
</items>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<items>
|
|
<item price="21" quantity="1">Widget A</item>
|
|
<item price="10.5" quantity="3">Widget B</item>
|
|
</items>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<names>
|
|
<n>john</n>
|
|
<n>mary</n>
|
|
</names>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<names>
|
|
<n>JOHN</n>
|
|
<n>MARY</n>
|
|
</names>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<config>
|
|
<settings>
|
|
<timeout>30</timeout>
|
|
<max_connections>100</max_connections>
|
|
</settings>
|
|
</config>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<config>
|
|
<settings>
|
|
<timeout>60</timeout>
|
|
<max_connections>200</max_connections>
|
|
</settings>
|
|
</config>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<store>
|
|
<department name="Electronics">
|
|
<product id="p1" stock="50">
|
|
<name>Laptop</name>
|
|
<price currency="USD">999.99</price>
|
|
</product>
|
|
<product id="p2" stock="100">
|
|
<name>Smartphone</name>
|
|
<price currency="USD">499.99</price>
|
|
</product>
|
|
</department>
|
|
<department name="Clothing">
|
|
<product id="p3" stock="200">
|
|
<name>T-Shirt</name>
|
|
<price currency="USD">19.99</price>
|
|
</product>
|
|
</department>
|
|
</store>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<store>
|
|
<department name="Electronics">
|
|
<product id="p1" stock="50">
|
|
<name>Laptop</name>
|
|
<price currency="USD">1199.988</price>
|
|
</product>
|
|
<product id="p2" stock="100">
|
|
<name>Smartphone</name>
|
|
<price currency="USD">599.988</price>
|
|
</product>
|
|
</department>
|
|
<department name="Clothing">
|
|
<product id="p3" stock="200">
|
|
<name>T-Shirt</name>
|
|
<price currency="USD">23.988</price>
|
|
</product>
|
|
</department>
|
|
</store>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<inventory>
|
|
<item id="1" stock="5" price="10.00"/>
|
|
<item id="2" stock="15" price="20.00"/>
|
|
<item id="3" stock="0" price="15.00"/>
|
|
</inventory>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<inventory>
|
|
<item id="1" stock="5" price="8.00"></item>
|
|
<item id="2" stock="15" price="16.00"></item>
|
|
<item id="3" stock="0" price="15.00"></item>
|
|
</inventory>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<data>
|
|
<entry>This & that</entry>
|
|
<entry>a < b</entry>
|
|
<entry>c > d</entry>
|
|
<entry>Quote: "Hello"</entry>
|
|
</data>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<data>
|
|
<entry>THIS & THAT</entry>
|
|
<entry>A < B</entry>
|
|
<entry>C > D</entry>
|
|
<entry>QUOTE: "HELLO"</entry>
|
|
</data>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<products>
|
|
<product>
|
|
<name>Widget</name>
|
|
<price>100</price>
|
|
<stock>20</stock>
|
|
</product>
|
|
</products>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<products>
|
|
<product>
|
|
<name>Widget</name>
|
|
<price>103.5</price>
|
|
<stock>20</stock>
|
|
</product>
|
|
</products>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<measurements>
|
|
<measurement>3.14159</measurement>
|
|
<measurement>2.71828</measurement>
|
|
<measurement>1.41421</measurement>
|
|
</measurements>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<measurements>
|
|
<measurement>3</measurement>
|
|
<measurement>3</measurement>
|
|
<measurement>1</measurement>
|
|
</measurements>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<contacts>
|
|
<contact>
|
|
<name>John Doe</name>
|
|
<email>john.doe@example.com</email>
|
|
<phone>123-456-7890</phone>
|
|
</contact>
|
|
</contacts>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<contacts>
|
|
<contact>
|
|
<name>John Doe</name>
|
|
<email>johndoe@anon.com</email>
|
|
<phone>123-XXX-XXXX</phone>
|
|
</contact>
|
|
</contacts>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<events>
|
|
<event>
|
|
<name>Conference</name>
|
|
<date>2023-06-15</date>
|
|
</event>
|
|
<event>
|
|
<name>Workshop</name>
|
|
<date>2023-06-20</date>
|
|
</event>
|
|
</events>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<events>
|
|
<event>
|
|
<name>Conference</name>
|
|
<date>2023-07-15</date>
|
|
</event>
|
|
<event>
|
|
<name>Workshop</name>
|
|
<date>2023-07-20</date>
|
|
</event>
|
|
</events>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<unclosed>
|
|
</root>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</element>
|
|
</root>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>123</element>
|
|
</root>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<library>
|
|
<books>
|
|
<book category="fiction" year="2020">
|
|
<title>The Imaginary World</title>
|
|
<author>Alice Johnson</author>
|
|
<price>19.99</price>
|
|
</book>
|
|
<book category="non-fiction" year="2019">
|
|
<title>History of Science</title>
|
|
<author>Bob Smith</author>
|
|
<price>29.99</price>
|
|
</book>
|
|
<book category="fiction" year="2021">
|
|
<title>Future Tales</title>
|
|
<author>Charlie Adams</author>
|
|
<price>24.99</price>
|
|
</book>
|
|
</books>
|
|
</library>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<library>
|
|
<books>
|
|
<book category="fiction" year="2020">
|
|
<title>The Imaginary World</title>
|
|
<author>Alice Johnson</author>
|
|
<price>15.99</price>
|
|
</book>
|
|
<book category="non-fiction" year="2019">
|
|
<title>History of Science</title>
|
|
<author>Bob Smith</author>
|
|
<price>29.99</price>
|
|
</book>
|
|
<book category="fiction" year="2021">
|
|
<title>Future Tales</title>
|
|
<author>Charlie Adams</author>
|
|
<price>19.99</price>
|
|
</book>
|
|
</books>
|
|
</library>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<game>
|
|
<characters>
|
|
<character id="hero">
|
|
<stats>
|
|
<strength>10</strength>
|
|
<agility>15</agility>
|
|
<intelligence>12</intelligence>
|
|
</stats>
|
|
<equipment>
|
|
<weapon damage="5">Sword</weapon>
|
|
<armor defense="3">Leather</armor>
|
|
</equipment>
|
|
</character>
|
|
<character id="villain">
|
|
<stats>
|
|
<strength>14</strength>
|
|
<agility>10</agility>
|
|
<intelligence>16</intelligence>
|
|
</stats>
|
|
<equipment>
|
|
<weapon damage="7">Axe</weapon>
|
|
<armor defense="5">Chain Mail</armor>
|
|
</equipment>
|
|
</character>
|
|
</characters>
|
|
</game>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<game>
|
|
<characters>
|
|
<character id="hero">
|
|
<stats>
|
|
<strength>12</strength>
|
|
<agility>18</agility>
|
|
<intelligence>14</intelligence>
|
|
</stats>
|
|
<equipment>
|
|
<weapon damage="7">Sword</weapon>
|
|
<armor defense="5">Leather</armor>
|
|
</equipment>
|
|
</character>
|
|
<character id="villain">
|
|
<stats>
|
|
<strength>14</strength>
|
|
<agility>10</agility>
|
|
<intelligence>16</intelligence>
|
|
</stats>
|
|
<equipment>
|
|
<weapon damage="7">Axe</weapon>
|
|
<armor defense="5">Chain Mail</armor>
|
|
</equipment>
|
|
</character>
|
|
</characters>
|
|
</game>`
|
|
|
|
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<items>
|
|
<item type="fruit">
|
|
<name>Apple</name>
|
|
<price>1.99</price>
|
|
<quantity>10</quantity>
|
|
</item>
|
|
<item type="vegetable">
|
|
<name>Carrot</name>
|
|
<price>0.99</price>
|
|
<quantity>5</quantity>
|
|
</item>
|
|
</items>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<items>
|
|
<item type="fruit">
|
|
<name>Apple</name>
|
|
<price>1.99</price>
|
|
<quantity>10</quantity>
|
|
<total>19.90</total>
|
|
</item>
|
|
<item type="vegetable">
|
|
<name>Carrot</name>
|
|
<price>0.99</price>
|
|
<quantity>5</quantity>
|
|
<total>4.95</total>
|
|
</item>
|
|
</items>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<products>
|
|
<product id="p1">
|
|
<name>Laptop</name>
|
|
<price>999.99</price>
|
|
<inStock>true</inStock>
|
|
</product>
|
|
<product id="p2">
|
|
<name>Phone</name>
|
|
<price>499.99</price>
|
|
<inStock>false</inStock>
|
|
</product>
|
|
</products>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<products>
|
|
<product id="p1" status="available">
|
|
<name>Laptop</name>
|
|
<price>999.99</price>
|
|
<inStock>true</inStock>
|
|
</product>
|
|
<product id="p2" status="out-of-stock">
|
|
<name>Phone</name>
|
|
<price>499.99</price>
|
|
<inStock>false</inStock>
|
|
</product>
|
|
</products>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<users>
|
|
<user>
|
|
<name>John Smith</name>
|
|
<email>john@example.com</email>
|
|
<password>secret123</password>
|
|
<role>admin</role>
|
|
</user>
|
|
<user>
|
|
<name>Jane Doe</name>
|
|
<email>jane@example.com</email>
|
|
<password>pass456</password>
|
|
<role>user</role>
|
|
</user>
|
|
</users>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<users>
|
|
<user>
|
|
<name>John Smith</name>
|
|
<email>john@example.com</email>
|
|
<role>admin</role>
|
|
</user>
|
|
<user>
|
|
<name>Jane Doe</name>
|
|
<email>jane@example.com</email>
|
|
<role>user</role>
|
|
</user>
|
|
</users>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<playlist>
|
|
<song>
|
|
<artist>Bob Dylan</artist>
|
|
<title>Blowin' in the Wind</title>
|
|
<year>1963</year>
|
|
</song>
|
|
<song>
|
|
<artist>The Beatles</artist>
|
|
<title>Hey Jude</title>
|
|
<year>1968</year>
|
|
</song>
|
|
</playlist>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<playlist>
|
|
<song>
|
|
<title>Blowin' in the Wind</title>
|
|
<artist>Bob Dylan</artist>
|
|
<year>1963</year>
|
|
</song>
|
|
<song>
|
|
<title>Hey Jude</title>
|
|
<artist>The Beatles</artist>
|
|
<year>1968</year>
|
|
</song>
|
|
</playlist>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<bookstore>
|
|
<book category="fiction">
|
|
<title>The Great Gatsby</title>
|
|
<author>F. Scott Fitzgerald</author>
|
|
<year>1925</year>
|
|
<price>10.99</price>
|
|
</book>
|
|
<book category="non-fiction">
|
|
<title>A Brief History of Time</title>
|
|
<author>Stephen Hawking</author>
|
|
<year>1988</year>
|
|
<price>15.99</price>
|
|
</book>
|
|
</bookstore>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<bookstore>
|
|
<book>
|
|
<details>
|
|
<title>The Great Gatsby</title>
|
|
<author>F. Scott Fitzgerald</author>
|
|
<year>1925</year>
|
|
</details>
|
|
<pricing>
|
|
<price currency="USD">10.99</price>
|
|
<discount>0</discount>
|
|
</pricing>
|
|
<metadata>
|
|
<category>fiction</category>
|
|
</metadata>
|
|
</book>
|
|
<book>
|
|
<details>
|
|
<title>A Brief History of Time</title>
|
|
<author>Stephen Hawking</author>
|
|
<year>1988</year>
|
|
</details>
|
|
<pricing>
|
|
<price currency="USD">15.99</price>
|
|
<discount>0</discount>
|
|
</pricing>
|
|
<metadata>
|
|
<category>non-fiction</category>
|
|
</metadata>
|
|
</book>
|
|
</bookstore>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<configuration>
|
|
<settings>
|
|
<setting name="timeout" value="30" />
|
|
<setting name="retries" value="3" />
|
|
<setting name="backoff" value="exponential" />
|
|
</settings>
|
|
<advanced>
|
|
<setting name="logging" value="debug" />
|
|
<setting name="timeout" value="60" />
|
|
</advanced>
|
|
</configuration>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<configuration>
|
|
<settings>
|
|
<setting name="timeout" value="60"></setting>
|
|
<setting name="retries" value="3"></setting>
|
|
<setting name="backoff" value="exponential"></setting>
|
|
</settings>
|
|
<advanced>
|
|
<setting name="logging" value="debug"></setting>
|
|
<setting name="timeout" value="120"></setting>
|
|
</advanced>
|
|
</configuration>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<data>
|
|
<settings>
|
|
<option name="debug" value="false"/>
|
|
<option name="log_level" value="info"/>
|
|
</settings>
|
|
</data>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<data>
|
|
<settings>
|
|
<option name="debug" value="false"/>
|
|
<option name="log_level" value="info"/>
|
|
</settings>
|
|
<calculated>
|
|
<stats>
|
|
<count>2</count>
|
|
<summary>Debug: OFF, Logging: info</summary>
|
|
</stats>
|
|
</calculated>
|
|
</data>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<library>
|
|
<books>
|
|
<book>
|
|
<title>Book 1</title>
|
|
<pages>200</pages>
|
|
</book>
|
|
<book>
|
|
<title>Book 2</title>
|
|
<pages>150</pages>
|
|
</book>
|
|
<book>
|
|
<title>Book 3</title>
|
|
<pages>300</pages>
|
|
</book>
|
|
</books>
|
|
</library>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<library>
|
|
<books>
|
|
<book>
|
|
<title>Book 3</title>
|
|
<pages>300</pages>
|
|
</book>
|
|
<book>
|
|
<title>Book 1</title>
|
|
<pages>200</pages>
|
|
</book>
|
|
</books>
|
|
<summary>
|
|
<count>2</count>
|
|
<total_pages>500</total_pages>
|
|
<average_pages>250</average_pages>
|
|
</summary>
|
|
</library>`
|
|
|
|
// 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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<config>
|
|
<database>
|
|
<connection>
|
|
<host>localhost</host>
|
|
<port>3306</port>
|
|
<credentials>
|
|
<username>admin</username>
|
|
<password>secret</password>
|
|
</credentials>
|
|
</connection>
|
|
<pool>
|
|
<size>10</size>
|
|
<timeout>30</timeout>
|
|
</pool>
|
|
</database>
|
|
<logging>
|
|
<level>info</level>
|
|
<file>/var/log/app.log</file>
|
|
</logging>
|
|
</config>
|
|
</application>`
|
|
|
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<application>
|
|
<config>
|
|
<database>
|
|
<connection>
|
|
<host>db.example.com</host>
|
|
<port>5432</port>
|
|
<credentials>
|
|
<username>admin</username>
|
|
<password>REDACTED</password>
|
|
</credentials>
|
|
</connection>
|
|
<pool>
|
|
<size>20</size>
|
|
<timeout>60</timeout>
|
|
</pool>
|
|
</database>
|
|
<logging>
|
|
<level>debug</level>
|
|
<file>/var/log/app.log</file>
|
|
</logging>
|
|
</config>
|
|
<status>
|
|
<environment>production</environment>
|
|
<updated>true</updated>
|
|
<secure>true</secure>
|
|
</status>
|
|
</application>`
|
|
|
|
// 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 := `
|
|
<root id="1">
|
|
<person name="John" age="30">
|
|
<address type="home">
|
|
<street>123 Main St</street>
|
|
<city>Anytown</city>
|
|
<zip>12345</zip>
|
|
</address>
|
|
<contact type="email">john@example.com</contact>
|
|
</person>
|
|
<person name="Jane" age="28">
|
|
<address type="work">
|
|
<street>456 Business Ave</street>
|
|
<city>Worktown</city>
|
|
<zip>54321</zip>
|
|
</address>
|
|
<contact type="phone">555-1234</contact>
|
|
</person>
|
|
</root>
|
|
`
|
|
|
|
// 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
|
|
table, err := processor.ToLua(L, root)
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert to Lua: %v", err)
|
|
}
|
|
L.SetGlobal("v", table)
|
|
|
|
// 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
|
|
table, err := processor.ToLua(L, street)
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert to Lua: %v", err)
|
|
}
|
|
L.SetGlobal("v", table)
|
|
|
|
// 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 := `<test>Original Text</test>`
|
|
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 := `<test id="old">Text</test>`
|
|
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")
|
|
}
|
|
})
|
|
}
|