Skip to content

Commit 6975514

Browse files
authored
Add comparison of element attributes (#3)
1 parent 4097ca8 commit 6975514

File tree

7 files changed

+240
-16
lines changed

7 files changed

+240
-16
lines changed

.travis.yml

+15-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
language: scala
22
sudo: false
33

4-
scala:
5-
- 2.11.11
6-
- 2.12.7
7-
- 2.13.0-M5
84
jdk:
9-
- oraclejdk8
5+
- openjdk8
6+
- openjdk11
107

11-
# cache config taken from http://www.scala-sbt.org/0.13/docs/Travis-CI-with-sbt.html
128
cache:
139
directories:
14-
- $HOME/.ivy2/cache
15-
- $HOME/.sbt/boot/
10+
- $HOME/.cache
11+
- $HOME/.sbt
1612

1713
before_cache:
18-
- find $HOME/.ivy2 -name "ivydata-*.properties" -delete
14+
- find $HOME/.ivy2 -name "*.lock" -delete
1915
- find $HOME/.sbt -name "*.lock" -delete
2016

2117
# build with scoverage report and upload to codecov
22-
script:
23-
- sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate
24-
after_success:
25-
- bash <(curl -s https://codecov.io/bash)
18+
19+
jobs:
20+
include:
21+
# run tests against all Scala versions & JDKs
22+
- stage: Test
23+
script: sbt +test
24+
# coverage only runs on 2.12 openjdk8
25+
- stage: Coverage
26+
script: sbt ++2.12.10 coverage test coverageReport coverageAggregate && bash <(curl -s https://codecov.io/bash)
27+

build.sbt

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ lazy val root = Project("scala-xml-compare", file("."))
6060
// site/paradox
6161
siteSubdirName in ScalaUnidoc := "api",
6262
addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc),
63-
paradoxProperties in Paradox ++= Map(
64-
"scaladoc.software.purpledragon.xml.base_url" -> ".../api"
63+
paradoxProperties in Compile ++= Map(
64+
"scaladoc.base_url" -> ".../api"
6565
),
66+
paradoxNavigationDepth := 3,
6667
scalacOptions in Compile in doc ++= Seq(
6768
"-doc-root-content",
6869
baseDirectory.value + "/root-scaladoc.txt"

project/plugins.sbt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// code style
22
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
3-
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0-M5")
3+
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0")
44
addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15")
55
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.0.0")
66

src/main/paradox/comparing-xml.md

+119
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,127 @@
11
# Comparing XML
22

3+
A simple XML comparison can be performed using
4+
@scaladoc[`XmlCompare.compare`](software.purpledragon.xml.compare.XmlCompare$):
5+
6+
```scala
7+
// using XML literals:
8+
val result = XmlCompare.compare(<test/>, <test/>) // == XmlEqual
9+
10+
// or from a file:
11+
val success = XmlCompare.compare(<result><success/></result>, XML.loadFile("result.xml"))
12+
```
13+
14+
The way that XML is compared can be customised by supplying a `Set` of
15+
@scaladoc[`DiffOption`s](software.purpledragon.xml.compare.options.DiffOption$):
16+
17+
```scala
18+
// this would result an XmlEqual
19+
XmlCompare.compare(
20+
<test name="John" age="45"/>,
21+
<test age="45" name="John"/>)
22+
23+
// this would result an XmlDiffers
24+
XmlCompare.compare(
25+
<test name="John" age="45"/>,
26+
<test age="45" name="John"/>,
27+
Set(DiffOption.StrictAttributeOrdering))
28+
```
29+
30+
@ref:[Comparison Options](#comparison-options) contains details of the supported options.
31+
32+
## Comparison Results
33+
34+
`XmlCompare.compare` returns an @scaladoc[`XmlDiff`](software.purpledragon.xml.compare.XmlDiff) that will either be
35+
@scaladoc[`XmlEqual`](software.purpledragon.xml.compare.XmlEqual) or a detailed
36+
@scaladoc[`XmlDiffers`](software.purpledragon.xml.compare.XmlDiffers). If a simple
37+
pass/fail check is required then the `isEqual` method can be called on the result.
38+
39+
### XmlEqual
40+
41+
An `XmlEqual` result signifies that no differences in the XML were found.
42+
43+
### XmlDiffers
44+
45+
An `XmlDiffers` result will contain the _first_ difference found in the XML. The `reason` property will have a
46+
human-readable reason for the difference, `left` & `right` will have the differences and `failurePath` will have the
47+
path segments to the difference.
48+
49+
For example:
50+
51+
```scala
52+
XmlDiffers(
53+
"different attribute ordering",
54+
Seq("first", "second"),
55+
Seq("second", "first"),
56+
Seq("test")
57+
)
58+
```
359

460
## Comparison Options
561

662
By default the following options are used:
763

864
* `IgnoreNamespacePrefix`
65+
66+
### IgnoreNamespacePrefix
67+
68+
If enabled the prefixes associated with namespaces will be ignored. Differing namespaces will still cause a comparison
69+
error.
70+
71+
#### Example
72+
73+
This:
74+
```xml
75+
<t:example xmlns:t="http://example.com">5</t:example>
76+
```
77+
78+
would be considered equal to:
79+
```xml
80+
<f:example xmlns:f="http://example.com">5</f:example>
81+
```
82+
83+
### IgnoreNamespace
84+
85+
If enabled then namespaces will be ignored completely.
86+
87+
#### Example
88+
89+
This:
90+
```xml
91+
<t:example xmlns:t="http://example.com">5</t:example>
92+
```
93+
94+
would be considered equal to:
95+
```xml
96+
<f:example xmlns:f="http://example.org">5</f:example>
97+
```
98+
99+
### StrictAttributeOrdering
100+
101+
This adds an additional comparison on the ordering of element attributes. The presence of attributes will be checked
102+
_before_ the ordering.
103+
104+
#### Example
105+
106+
This:
107+
108+
```xml
109+
<example first="a" second="b" />
110+
```
111+
112+
would not be equal to:
113+
114+
```xml
115+
<example second="b" first="a" />
116+
```
117+
118+
and would result in the following failure:
119+
120+
```scala
121+
XmlDiffers(
122+
"different attribute ordering",
123+
Seq("first", "second"),
124+
Seq("second", "first"),
125+
Seq("test")
126+
)
127+
```

xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala

+24
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ object XmlCompare {
5151
private def compareNodes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
5252
val checks: Seq[Check] = Seq(
5353
compareNamespace,
54+
compareAttributes,
5455
compareText,
5556
compareChildren
5657
)
@@ -78,6 +79,29 @@ object XmlCompare {
7879
}
7980
}
8081

82+
private def compareAttributes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
83+
def extractAttributes(node: Node): (Seq[String], Map[String, String]) = {
84+
node.attributes.foldLeft(Seq.empty[String], Map.empty[String, String]) {
85+
case ((keys, attribs), attrib) =>
86+
(keys :+ attrib.key, attribs + (attrib.key -> attrib.value.text))
87+
}
88+
}
89+
90+
val (leftKeys, leftMap) = extractAttributes(left)
91+
val (rightKeys, rightMap) = extractAttributes(right)
92+
93+
if (leftKeys.sorted != rightKeys.sorted) {
94+
XmlDiffers("different attribute names", leftKeys.sorted, rightKeys.sorted, extendPath(path, left))
95+
} else if (options.contains(StrictAttributeOrdering) && leftKeys != rightKeys) {
96+
XmlDiffers("different attribute ordering", leftKeys, rightKeys, extendPath(path, left))
97+
} else {
98+
leftKeys.sorted collectFirst {
99+
case name if leftMap(name) != rightMap(name) =>
100+
XmlDiffers(s"different value for attribute '$name'", leftMap(name), rightMap(name), extendPath(path, left))
101+
} getOrElse XmlEqual
102+
}
103+
}
104+
81105
private def compareText(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
82106
def extractText(node: Node): String = node.child.collect({ case a: Atom[_] => a }).map(_.text.trim).mkString
83107
val leftText = extractText(left)

xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala

+14
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,18 @@ object DiffOption extends Enumeration {
4949
* }}}
5050
*/
5151
val IgnoreNamespace: DiffOption.Value = Value
52+
53+
/**
54+
* Require element attributes have the same ordering.
55+
*
56+
* Enabling this make this:
57+
* {{{
58+
* <example first="a" second="b" />
59+
* }}}
60+
* not equal to:
61+
* {{{
62+
* <example second="b" first="a" />
63+
* }}}
64+
*/
65+
val StrictAttributeOrdering: DiffOption.Value = Value
5266
}

xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala

+64
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2017 Michael Stringer
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package software.purpledragon.xml.compare
218

319
import org.scalatest.{FlatSpec, Matchers}
@@ -80,6 +96,41 @@ class XmlCompareSpec extends FlatSpec with Matchers {
8096
)
8197
}
8298

99+
it should "match with same attributes" in {
100+
XmlCompare.compare(<test first="a" second="b"/>, <test first="a" second="b" />) shouldBe XmlEqual
101+
}
102+
103+
it should "match with same attributes in different order" in {
104+
XmlCompare.compare(<test first="a" second="b"/>, <test second="b" first="a" />) shouldBe XmlEqual
105+
}
106+
107+
it should "not-match with different attribute names" in {
108+
XmlCompare.compare(<test value="a" />, <test cost="a" />) shouldBe XmlDiffers(
109+
"different attribute names",
110+
Seq("value"),
111+
Seq("cost"),
112+
Seq("test")
113+
)
114+
}
115+
116+
it should "not-match with different attribute count" in {
117+
XmlCompare.compare(<test first="a" second="b" />, <test first="a" />) shouldBe XmlDiffers(
118+
"different attribute names",
119+
Seq("first", "second"),
120+
Seq("first"),
121+
Seq("test")
122+
)
123+
}
124+
125+
it should "not-match with different attribute value" in {
126+
XmlCompare.compare(<test value="a" />, <test value="b" />) shouldBe XmlDiffers(
127+
"different value for attribute 'value'",
128+
"a",
129+
"b",
130+
Seq("test")
131+
)
132+
}
133+
83134
it should "not-match with multiple errors" in {
84135
XmlCompare.compare(
85136
<test><child-1>text-1</child-1><child-2>text-2</child-2></test>,
@@ -115,4 +166,17 @@ class XmlCompareSpec extends FlatSpec with Matchers {
115166
<t:test xmlns:e="http://example.org"/>,
116167
Set(IgnoreNamespace)) shouldBe XmlEqual
117168
}
169+
170+
"compare with StrictAttributeOrder" should "match with same attributes" in {
171+
XmlCompare.compare(<test first="a" second="b" />, <test first="a" second="b" />, Set(StrictAttributeOrdering)) shouldBe XmlEqual
172+
}
173+
174+
it should "not-match with attributes in different order" in {
175+
XmlCompare.compare(<test first="a" second="b" />, <test second="b" first="a" />, Set(StrictAttributeOrdering)) shouldBe XmlDiffers(
176+
"different attribute ordering",
177+
Seq("first", "second"),
178+
Seq("second", "first"),
179+
Seq("test")
180+
)
181+
}
118182
}

0 commit comments

Comments
 (0)