aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go
blob: 3b412ce818a7a7f9b120d9570070ab648b5eddec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package matchers

import (
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"reflect"
	"sort"
	"strings"

	"github.com/onsi/gomega/format"
	"golang.org/x/net/html/charset"
)

type MatchXMLMatcher struct {
	XMLToMatch interface{}
}

func (matcher *MatchXMLMatcher) Match(actual interface{}) (success bool, err error) {
	actualString, expectedString, err := matcher.formattedPrint(actual)
	if err != nil {
		return false, err
	}

	aval, err := parseXmlContent(actualString)
	if err != nil {
		return false, fmt.Errorf("Actual '%s' should be valid XML, but it is not.\nUnderlying error:%s", actualString, err)
	}

	eval, err := parseXmlContent(expectedString)
	if err != nil {
		return false, fmt.Errorf("Expected '%s' should be valid XML, but it is not.\nUnderlying error:%s", expectedString, err)
	}

	return reflect.DeepEqual(aval, eval), nil
}

func (matcher *MatchXMLMatcher) FailureMessage(actual interface{}) (message string) {
	actualString, expectedString, _ := matcher.formattedPrint(actual)
	return fmt.Sprintf("Expected\n%s\nto match XML of\n%s", actualString, expectedString)
}

func (matcher *MatchXMLMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	actualString, expectedString, _ := matcher.formattedPrint(actual)
	return fmt.Sprintf("Expected\n%s\nnot to match XML of\n%s", actualString, expectedString)
}

func (matcher *MatchXMLMatcher) formattedPrint(actual interface{}) (actualString, expectedString string, err error) {
	var ok bool
	actualString, ok = toString(actual)
	if !ok {
		return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte.  Got actual:\n%s", format.Object(actual, 1))
	}
	expectedString, ok = toString(matcher.XMLToMatch)
	if !ok {
		return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte.  Got expected:\n%s", format.Object(matcher.XMLToMatch, 1))
	}
	return actualString, expectedString, nil
}

func parseXmlContent(content string) (*xmlNode, error) {
	allNodes := []*xmlNode{}

	dec := newXmlDecoder(strings.NewReader(content))
	for {
		tok, err := dec.Token()
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, fmt.Errorf("failed to decode next token: %v", err)
		}

		lastNodeIndex := len(allNodes) - 1
		var lastNode *xmlNode
		if len(allNodes) > 0 {
			lastNode = allNodes[lastNodeIndex]
		} else {
			lastNode = &xmlNode{}
		}

		switch tok := tok.(type) {
		case xml.StartElement:
			attrs := attributesSlice(tok.Attr)
			sort.Sort(attrs)
			allNodes = append(allNodes, &xmlNode{XMLName: tok.Name, XMLAttr: tok.Attr})
		case xml.EndElement:
			if len(allNodes) > 1 {
				allNodes[lastNodeIndex-1].Nodes = append(allNodes[lastNodeIndex-1].Nodes, lastNode)
				allNodes = allNodes[:lastNodeIndex]
			}
		case xml.CharData:
			lastNode.Content = append(lastNode.Content, tok.Copy()...)
		case xml.Comment:
			lastNode.Comments = append(lastNode.Comments, tok.Copy())
		case xml.ProcInst:
			lastNode.ProcInsts = append(lastNode.ProcInsts, tok.Copy())
		}
	}

	if len(allNodes) == 0 {
		return nil, errors.New("found no nodes")
	}
	firstNode := allNodes[0]
	trimParentNodesContentSpaces(firstNode)

	return firstNode, nil
}

func newXmlDecoder(reader io.Reader) *xml.Decoder {
	dec := xml.NewDecoder(reader)
	dec.CharsetReader = charset.NewReaderLabel
	return dec
}

func trimParentNodesContentSpaces(node *xmlNode) {
	if len(node.Nodes) > 0 {
		node.Content = bytes.TrimSpace(node.Content)
		for _, childNode := range node.Nodes {
			trimParentNodesContentSpaces(childNode)
		}
	}
}

type xmlNode struct {
	XMLName   xml.Name
	Comments  []xml.Comment
	ProcInsts []xml.ProcInst
	XMLAttr   []xml.Attr
	Content   []byte
	Nodes     []*xmlNode
}