diff --git a/processor/xpath/xpath.go b/processor/xpath/xpath.go
new file mode 100644
index 0000000..9fc03d2
--- /dev/null
+++ b/processor/xpath/xpath.go
@@ -0,0 +1,98 @@
+package xpath
+
+import "errors"
+
+// XPathStep represents a single step in an XPath expression
+type XPathStep struct {
+ Type StepType
+ Name string
+ Predicate *Predicate
+}
+
+// StepType defines the type of XPath step
+type StepType int
+
+const (
+ // RootStep represents the root step (/)
+ RootStep StepType = iota
+ // ChildStep represents a child element step (element)
+ ChildStep
+ // RecursiveDescentStep represents a recursive descent step (//)
+ RecursiveDescentStep
+ // WildcardStep represents a wildcard step (*)
+ WildcardStep
+ // PredicateStep represents a predicate condition step ([...])
+ PredicateStep
+)
+
+// PredicateType defines the type of XPath predicate
+type PredicateType int
+
+const (
+ // IndexPredicate represents an index predicate [n]
+ IndexPredicate PredicateType = iota
+ // LastPredicate represents a last() function predicate
+ LastPredicate
+ // LastMinusPredicate represents a last()-n predicate
+ LastMinusPredicate
+ // PositionPredicate represents position()-based predicates
+ PositionPredicate
+ // AttributeExistsPredicate represents [@attr] predicate
+ AttributeExistsPredicate
+ // AttributeEqualsPredicate represents [@attr='value'] predicate
+ AttributeEqualsPredicate
+ // ComparisonPredicate represents element comparison predicates
+ ComparisonPredicate
+)
+
+// Predicate represents a condition in XPath
+type Predicate struct {
+ Type PredicateType
+ Index int
+ Offset int
+ Attribute string
+ Value string
+ Expression string
+}
+
+// XMLNode represents a node in the result set with its value and path
+type XMLNode struct {
+ Value interface{}
+ Path string
+}
+
+// ParseXPath parses an XPath expression into a series of steps
+func ParseXPath(path string) ([]XPathStep, error) {
+ if path == "" {
+ return nil, errors.New("empty path")
+ }
+
+ // This is just a placeholder implementation for the tests
+ // The actual implementation would parse the XPath expression
+ return nil, errors.New("not implemented")
+}
+
+// Get retrieves nodes from XML data using an XPath expression
+func Get(data interface{}, path string) ([]XMLNode, error) {
+ if data == "" {
+ return nil, errors.New("empty XML data")
+ }
+
+ // This is just a placeholder implementation for the tests
+ // The actual implementation would evaluate the XPath against the XML
+ return nil, errors.New("not implemented")
+}
+
+// Set updates a node in the XML data using an XPath expression
+func Set(xmlData string, path string, value interface{}) (string, error) {
+ // This is just a placeholder implementation for the tests
+ // The actual implementation would modify the XML based on the XPath
+ return "", errors.New("not implemented")
+}
+
+// SetAll updates all nodes matching an XPath expression in the XML data
+func SetAll(xmlData string, path string, value interface{}) (string, error) {
+ // This is just a placeholder implementation for the tests
+ // The actual implementation would modify all matching nodes
+ return "", errors.New("not implemented")
+}
diff --git a/processor/xpath/xpath_test.go b/processor/xpath/xpath_test.go
new file mode 100644
index 0000000..44c3e90
--- /dev/null
+++ b/processor/xpath/xpath_test.go
@@ -0,0 +1,545 @@
+package xpath
+
+import (
+ "reflect"
+ "testing"
+)
+
+// XML test data as a string for our tests
+var testXML = `
+
+
+ The Fellowship of the Ring
+ J.R.R. Tolkien
+ 1954
+ 22.99
+
+
+ The Two Towers
+ J.R.R. Tolkien
+ 1954
+ 23.45
+
+
+ Learning XML
+ Erik T. Ray
+ 2003
+ 39.95
+
+
+ red
+ 199.95
+
+
+`
+
+func TestParser(t *testing.T) {
+ tests := []struct {
+ path string
+ steps []XPathStep
+ wantErr bool
+ }{
+ {
+ path: "/store/bicycle/color",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "bicycle"},
+ {Type: ChildStep, Name: "color"},
+ },
+ },
+ {
+ path: "//price",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: RecursiveDescentStep, Name: "price"},
+ },
+ },
+ {
+ path: "/store/book/*",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: WildcardStep},
+ },
+ },
+ {
+ path: "/store/book[1]/title",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: PredicateStep, Predicate: &Predicate{Type: IndexPredicate, Index: 1}},
+ {Type: ChildStep, Name: "title"},
+ },
+ },
+ {
+ path: "//title[@lang]",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: RecursiveDescentStep, Name: "title"},
+ {Type: PredicateStep, Predicate: &Predicate{Type: AttributeExistsPredicate, Attribute: "lang"}},
+ },
+ },
+ {
+ path: "//title[@lang='en']",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: RecursiveDescentStep, Name: "title"},
+ {Type: PredicateStep, Predicate: &Predicate{
+ Type: AttributeEqualsPredicate,
+ Attribute: "lang",
+ Value: "en",
+ }},
+ },
+ },
+ {
+ path: "/store/book[price>35.00]/title",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: PredicateStep, Predicate: &Predicate{
+ Type: ComparisonPredicate,
+ Expression: "price>35.00",
+ }},
+ {Type: ChildStep, Name: "title"},
+ },
+ },
+ {
+ path: "/store/book[last()]",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: PredicateStep, Predicate: &Predicate{Type: LastPredicate}},
+ },
+ },
+ {
+ path: "/store/book[last()-1]",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: PredicateStep, Predicate: &Predicate{
+ Type: LastMinusPredicate,
+ Offset: 1,
+ }},
+ },
+ },
+ {
+ path: "/store/book[position()<3]",
+ steps: []XPathStep{
+ {Type: RootStep},
+ {Type: ChildStep, Name: "store"},
+ {Type: ChildStep, Name: "book"},
+ {Type: PredicateStep, Predicate: &Predicate{
+ Type: PositionPredicate,
+ Expression: "position()<3",
+ }},
+ },
+ },
+ {
+ path: "invalid/path",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.path, func(t *testing.T) {
+ steps, err := ParseXPath(tt.path)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("ParseXPath() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
+ t.Errorf("ParseXPath() steps = %+v, want %+v", steps, tt.steps)
+ }
+ })
+ }
+}
+
+func TestEvaluator(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ expected []XMLNode
+ error bool
+ }{
+ {
+ name: "simple_element_access",
+ path: "/store/bicycle/color",
+ expected: []XMLNode{
+ {Value: "red", Path: "/store/bicycle/color"},
+ },
+ },
+ {
+ name: "recursive_element_access",
+ path: "//price",
+ expected: []XMLNode{
+ {Value: "22.99", Path: "/store/book[1]/price"},
+ {Value: "23.45", Path: "/store/book[2]/price"},
+ {Value: "39.95", Path: "/store/book[3]/price"},
+ {Value: "199.95", Path: "/store/bicycle/price"},
+ },
+ },
+ {
+ name: "wildcard_element_access",
+ path: "/store/book[1]/*",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ {Value: "J.R.R. Tolkien", Path: "/store/book[1]/author"},
+ {Value: "1954", Path: "/store/book[1]/year"},
+ {Value: "22.99", Path: "/store/book[1]/price"},
+ },
+ },
+ {
+ name: "indexed_element_access",
+ path: "/store/book[1]/title",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ },
+ },
+ {
+ name: "attribute_exists_predicate",
+ path: "//title[@lang]",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ {Value: "The Two Towers", Path: "/store/book[2]/title"},
+ {Value: "Learning XML", Path: "/store/book[3]/title"},
+ },
+ },
+ {
+ name: "attribute_equals_predicate",
+ path: "//title[@lang='en']",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ {Value: "The Two Towers", Path: "/store/book[2]/title"},
+ {Value: "Learning XML", Path: "/store/book[3]/title"},
+ },
+ },
+ {
+ name: "value_comparison_predicate",
+ path: "/store/book[price>35.00]/title",
+ expected: []XMLNode{
+ {Value: "Learning XML", Path: "/store/book[3]/title"},
+ },
+ },
+ {
+ name: "last_predicate",
+ path: "/store/book[last()]/title",
+ expected: []XMLNode{
+ {Value: "Learning XML", Path: "/store/book[3]/title"},
+ },
+ },
+ {
+ name: "last_minus_predicate",
+ path: "/store/book[last()-1]/title",
+ expected: []XMLNode{
+ {Value: "The Two Towers", Path: "/store/book[2]/title"},
+ },
+ },
+ {
+ name: "position_predicate",
+ path: "/store/book[position()<3]/title",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ {Value: "The Two Towers", Path: "/store/book[2]/title"},
+ },
+ },
+ {
+ name: "all_elements",
+ path: "//*",
+ expected: []XMLNode{
+ // For brevity, we'll just check the count, not all values
+ },
+ },
+ {
+ name: "invalid_index",
+ path: "/store/book[10]/title",
+ expected: []XMLNode{},
+ error: true,
+ },
+ {
+ name: "nonexistent_element",
+ path: "/store/nonexistent",
+ expected: []XMLNode{},
+ error: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := Get(testXML, tt.path)
+ if err != nil {
+ if !tt.error {
+ t.Errorf("Get() returned error: %v", err)
+ }
+ return
+ }
+
+ // Special handling for the "//*" test case
+ if tt.path == "//*" {
+ // Just check that we got multiple elements, not the specific count
+ if len(result) < 10 { // We expect at least 10 elements
+ t.Errorf("Expected multiple elements for '//*', got %d", len(result))
+ }
+ return
+ }
+
+ if len(result) != len(tt.expected) {
+ t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
+ return
+ }
+
+ // Validate both values and paths
+ for i, e := range tt.expected {
+ if i < len(result) {
+ if !reflect.DeepEqual(result[i].Value, e.Value) {
+ t.Errorf("Value at [%d]: got %v, expected %v", i, result[i].Value, e.Value)
+ }
+ if result[i].Path != e.Path {
+ t.Errorf("Path at [%d]: got %s, expected %s", i, result[i].Path, e.Path)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestEdgeCases(t *testing.T) {
+ t.Run("empty_data", func(t *testing.T) {
+ result, err := Get("", "/store/book")
+ if err == nil {
+ t.Errorf("Expected error for empty data")
+ return
+ }
+ if len(result) > 0 {
+ t.Errorf("Expected empty result, got %v", result)
+ }
+ })
+
+ t.Run("empty_path", func(t *testing.T) {
+ _, err := ParseXPath("")
+ if err == nil {
+ t.Error("Expected error for empty path")
+ }
+ })
+
+ t.Run("invalid_xml", func(t *testing.T) {
+ _, err := Get("xml", "/store")
+ if err == nil {
+ t.Error("Expected error for invalid XML")
+ }
+ })
+
+ t.Run("current_node", func(t *testing.T) {
+ result, err := Get(testXML, "/store/book[1]/.")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 1 {
+ t.Errorf("Expected 1 result, got %d", len(result))
+ }
+ })
+
+ t.Run("attributes", func(t *testing.T) {
+ result, err := Get(testXML, "/store/book[1]/title/@lang")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 1 || result[0].Value != "en" {
+ t.Errorf("Expected 'en', got %v", result)
+ }
+ })
+}
+
+func TestGetWithPaths(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ expected []XMLNode
+ }{
+ {
+ name: "simple_element_access",
+ path: "/store/bicycle/color",
+ expected: []XMLNode{
+ {Value: "red", Path: "/store/bicycle/color"},
+ },
+ },
+ {
+ name: "indexed_element_access",
+ path: "/store/book[1]/title",
+ expected: []XMLNode{
+ {Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
+ },
+ },
+ {
+ name: "recursive_element_access",
+ path: "//price",
+ expected: []XMLNode{
+ {Value: "22.99", Path: "/store/book[1]/price"},
+ {Value: "23.45", Path: "/store/book[2]/price"},
+ {Value: "39.95", Path: "/store/book[3]/price"},
+ {Value: "199.95", Path: "/store/bicycle/price"},
+ },
+ },
+ {
+ name: "attribute_access",
+ path: "/store/book[1]/title/@lang",
+ expected: []XMLNode{
+ {Value: "en", Path: "/store/book[1]/title/@lang"},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := Get(testXML, tt.path)
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+
+ // Check if lengths match
+ if len(result) != len(tt.expected) {
+ t.Errorf("Get() returned %d items, expected %d", len(result), len(tt.expected))
+ return
+ }
+
+ // For each expected item, find its match in the results and verify both value and path
+ for _, expected := range tt.expected {
+ found := false
+ for _, r := range result {
+ // First verify the value matches
+ if reflect.DeepEqual(r.Value, expected.Value) {
+ found = true
+ // Then verify the path matches
+ if r.Path != expected.Path {
+ t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
+ }
+ break
+ }
+ }
+ if !found {
+ t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
+ }
+ }
+ })
+ }
+}
+
+func TestSet(t *testing.T) {
+ t.Run("simple element", func(t *testing.T) {
+ xmlData := `John`
+ newXML, err := Set(xmlData, "/root/name", "Jane")
+ if err != nil {
+ t.Errorf("Set() returned error: %v", err)
+ return
+ }
+
+ // Verify the change
+ result, err := Get(newXML, "/root/name")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 1 || result[0].Value != "Jane" {
+ t.Errorf("Set() failed: expected name to be 'Jane', got %v", result)
+ }
+ })
+
+ t.Run("attribute", func(t *testing.T) {
+ xmlData := ``
+ newXML, err := Set(xmlData, "/root/element/@id", "456")
+ if err != nil {
+ t.Errorf("Set() returned error: %v", err)
+ return
+ }
+
+ // Verify the change
+ result, err := Get(newXML, "/root/element/@id")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 1 || result[0].Value != "456" {
+ t.Errorf("Set() failed: expected id to be '456', got %v", result)
+ }
+ })
+
+ t.Run("indexed element", func(t *testing.T) {
+ xmlData := `- first
- second
`
+ newXML, err := Set(xmlData, "/root/items/item[1]", "changed")
+ if err != nil {
+ t.Errorf("Set() returned error: %v", err)
+ return
+ }
+
+ // Verify the change
+ result, err := Get(newXML, "/root/items/item[1]")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 1 || result[0].Value != "changed" {
+ t.Errorf("Set() failed: expected item to be 'changed', got %v", result)
+ }
+ })
+}
+
+func TestSetAll(t *testing.T) {
+ t.Run("multiple elements", func(t *testing.T) {
+ xmlData := `- first
- second
`
+ newXML, err := SetAll(xmlData, "//item", "changed")
+ if err != nil {
+ t.Errorf("SetAll() returned error: %v", err)
+ return
+ }
+
+ // Verify all items are changed
+ result, err := Get(newXML, "//item")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 2 {
+ t.Errorf("Expected 2 results, got %d", len(result))
+ return
+ }
+
+ for i, node := range result {
+ if node.Value != "changed" {
+ t.Errorf("Item %d not changed, got %v", i+1, node.Value)
+ }
+ }
+ })
+
+ t.Run("attributes", func(t *testing.T) {
+ xmlData := ` `
+ newXML, err := SetAll(xmlData, "//item/@id", "new")
+ if err != nil {
+ t.Errorf("SetAll() returned error: %v", err)
+ return
+ }
+
+ // Verify all attributes are changed
+ result, err := Get(newXML, "//item/@id")
+ if err != nil {
+ t.Errorf("Get() returned error: %v", err)
+ return
+ }
+ if len(result) != 2 {
+ t.Errorf("Expected 2 results, got %d", len(result))
+ return
+ }
+
+ for i, node := range result {
+ if node.Value != "new" {
+ t.Errorf("Attribute %d not changed, got %v", i+1, node.Value)
+ }
+ }
+ })
+}