diff --git a/project/plugins.sbt b/project/plugins.sbt
index 94168e4..75f26a9 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -14,3 +14,4 @@ addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.0")
addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.6.5")
+addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.0")
diff --git a/src/main/paradox/comparing-xml.md b/src/main/paradox/comparing-xml.md
index 0f82ff6..d5dbe73 100644
--- a/src/main/paradox/comparing-xml.md
+++ b/src/main/paradox/comparing-xml.md
@@ -124,4 +124,51 @@ XmlDiffers(
Seq("second", "first"),
Seq("test")
)
-```
\ No newline at end of file
+```
+
+### IgnoreChildOrder
+
+If enabled the ordering of child elements will be ignored. This is handled by re-ordering child nodes using an arbitrary
+sorting algorithm before comparing them.
+
+_Note: the first difference returned may be different if this option is enabled._
+
+#### Example 1
+
+This:
+
+```xml
+
+
+
+
+```
+
+would be considered equal to:
+```xml
+
+
+
+
+```
+
+#### Example 2
+
+This:
+
+```xml
+
+
+
+
+```
+
+would be considered equal to:
+```xml
+
+
+
+
+```
+
+_(The ordering of nodes and attributes are both ignored)_
\ No newline at end of file
diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/XmlUtils.scala b/xml-compare/src/main/scala/software/purpledragon/xml/XmlUtils.scala
new file mode 100644
index 0000000..649a367
--- /dev/null
+++ b/xml-compare/src/main/scala/software/purpledragon/xml/XmlUtils.scala
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Michael Stringer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package software.purpledragon.xml
+
+import scala.xml.Node
+
+/**
+ * Utilities for dealing with XML.
+ */
+object XmlUtils {
+
+ /**
+ * Extracts names of attributes and a map of attributes from an XML [[scala.xml.Node Node]].
+ *
+ * @param node the XML node to extract attributes for.
+ * @return a sequence of attribute names and a map of attribute values.
+ */
+ def extractAttributes(node: Node): (Seq[String], Map[String, String]) = {
+ node.attributes.foldLeft(Seq.empty[String], Map.empty[String, String]) {
+ case ((keys, attribs), attrib) =>
+ (keys :+ attrib.key, attribs + (attrib.key -> attrib.value.text))
+ }
+ }
+}
diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/NormalisedNodeOrdering.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/NormalisedNodeOrdering.scala
new file mode 100644
index 0000000..03f51e7
--- /dev/null
+++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/NormalisedNodeOrdering.scala
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Michael Stringer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package software.purpledragon.xml.compare
+
+import software.purpledragon.xml.XmlUtils.extractAttributes
+
+import scala.xml._
+
+/**
+ * Arbitrary ordering for XML nodes used to normalise XML when we are ignoring child ordering.
+ */
+private[compare] object NormalisedNodeOrdering extends Ordering[Node] {
+ private def typeToOrdering(node: Node): Int = {
+ node match {
+ case _: Elem => 1
+ case _: Text => 2
+ case _: PCData => 3
+ case _: Comment => 4
+ }
+ }
+
+ override def compare(x: Node, y: Node): Int = {
+ (x, y) match {
+ case (xe: Elem, ye: Elem) =>
+ val labelOrder = xe.label compareTo ye.label
+
+ if (labelOrder != 0) {
+ labelOrder
+ } else {
+ val (xAttributeNames, xAttributes) = extractAttributes(xe)
+ val (yAttributeNames, yAttributes) = extractAttributes(ye)
+
+ // order by attribute count
+ val attributeSizeOrder = xAttributeNames.size compareTo yAttributeNames.size
+
+ if (attributeSizeOrder != 0) {
+ attributeSizeOrder
+ } else {
+ // compare attribute names
+ val attributeNamesOrder = xAttributeNames.sorted zip yAttributeNames.sorted map {
+ case (x, y) => x compareTo y
+ }
+
+ // take first difference
+ attributeNamesOrder.find(_ != 0) match {
+ case Some(v) =>
+ v
+ case None =>
+ // if not compare values
+ val attributeValuesOrder = xAttributeNames map { name =>
+ xAttributes(name) compareTo yAttributes(name)
+ }
+
+ attributeValuesOrder.find(_ != 0).getOrElse(0)
+ }
+ }
+ }
+
+ case (xe: Text, ye: Text) =>
+ xe.text compareTo ye.text
+
+ case (xe: PCData, ye: PCData) =>
+ xe.data compareTo ye.data
+
+ case (xe: Comment, ye: Comment) =>
+ xe.commentText compareTo ye.commentText
+
+ case _ =>
+ // different types - order by type
+ typeToOrdering(x) compareTo typeToOrdering(y)
+ }
+ }
+}
diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala
index 4fe8c16..36e2e77 100644
--- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala
+++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala
@@ -16,10 +16,11 @@
package software.purpledragon.xml.compare
+import software.purpledragon.xml.XmlUtils.extractAttributes
import software.purpledragon.xml.compare.options.DiffOption._
-import software.purpledragon.xml.compare.options.DiffOptions
+import software.purpledragon.xml.compare.options.{DiffOption, DiffOptions}
-import scala.xml.{Atom, Node}
+import scala.xml._
/**
* Utility for comparing XML documents.
@@ -27,6 +28,8 @@ import scala.xml.{Atom, Node}
object XmlCompare {
private type Check = (Node, Node, DiffOptions, Seq[String]) => XmlDiff
+ private implicit val NodeOrdering = NormalisedNodeOrdering
+
/**
* Default [[software.purpledragon.xml.compare.options.DiffOption.DiffOption DiffOption]]s to use during XML comparison.
*
@@ -80,13 +83,6 @@ object XmlCompare {
}
private def compareAttributes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
- def extractAttributes(node: Node): (Seq[String], Map[String, String]) = {
- node.attributes.foldLeft(Seq.empty[String], Map.empty[String, String]) {
- case ((keys, attribs), attrib) =>
- (keys :+ attrib.key, attribs + (attrib.key -> attrib.value.text))
- }
- }
-
val (leftKeys, leftMap) = extractAttributes(left)
val (rightKeys, rightMap) = extractAttributes(right)
@@ -115,8 +111,8 @@ object XmlCompare {
}
private def compareChildren(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
- val leftChildren = left.child.filterNot(c => c.isInstanceOf[Atom[_]])
- val rightChildren = right.child.filterNot(c => c.isInstanceOf[Atom[_]])
+ val leftChildren = normalise(left.child, options)
+ val rightChildren = normalise(right.child, options)
if (leftChildren.size != rightChildren.size) {
XmlDiffers("different child count", leftChildren.size, rightChildren.size, extendPath(path, left))
@@ -136,4 +132,15 @@ object XmlCompare {
private def extendPath(path: Seq[String], node: Node): Seq[String] = {
path :+ node.nameToString(new StringBuilder()).toString
}
+
+ private def normalise(nodes: Seq[Node], options: DiffOptions): Seq[Node] = {
+ val sort = options.contains(DiffOption.IgnoreChildOrder)
+ val filtered = nodes.filterNot(n => n.isInstanceOf[Atom[_]])
+
+ if (sort) {
+ filtered.sorted
+ } else {
+ filtered
+ }
+ }
}
diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala
index 80960f0..669fed1 100644
--- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala
+++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala
@@ -63,4 +63,24 @@ object DiffOption extends Enumeration {
* }}}
*/
val StrictAttributeOrdering: DiffOption.Value = Value
+
+ /**
+ * Ignores the ordering of XML elements.
+ *
+ * Enabling this makes this:
+ * {{{
+ *
+ *
+ *
+ *
+ * }}}
+ * equal to:
+ * {{{
+ *
+ *
+ *
+ *
+ * }}}
+ */
+ val IgnoreChildOrder: DiffOption.Value = Value
}
diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/XmlUtilsSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/XmlUtilsSpec.scala
new file mode 100644
index 0000000..3c876e4
--- /dev/null
+++ b/xml-compare/src/test/scala/software/purpledragon/xml/XmlUtilsSpec.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Michael Stringer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package software.purpledragon.xml
+
+import org.scalatest.{FlatSpec, Matchers}
+
+class XmlUtilsSpec extends FlatSpec with Matchers {
+ "XmlUtils.extractAttributes" should "return empty values for no attributes" in {
+ XmlUtils.extractAttributes() shouldBe (Nil, Map.empty[String, String])
+ }
+
+ it should "return attribute names and value map" in {
+ val (names, attributes) = XmlUtils.extractAttributes()
+
+ names shouldBe Seq("field3", "field1", "field2")
+ attributes shouldBe Map(
+ "field1" -> "value-1",
+ "field2" -> "value-2",
+ "field3" -> "value-3"
+ )
+ }
+}
diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/compare/NormalisedNodeOrderingSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/compare/NormalisedNodeOrderingSpec.scala
new file mode 100644
index 0000000..508a811
--- /dev/null
+++ b/xml-compare/src/test/scala/software/purpledragon/xml/compare/NormalisedNodeOrderingSpec.scala
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 Michael Stringer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package software.purpledragon.xml.compare
+
+import org.scalatest.{FlatSpec, Matchers}
+
+import scala.xml.{Comment, Node, PCData, Text}
+
+class NormalisedNodeOrderingSpec extends FlatSpec with Matchers {
+ private implicit val ordering: Ordering[Node] = NormalisedNodeOrdering
+
+ "NormalisedNodeOrdering" should "order nodes by type" in {
+ val nodes = Seq[Node](
+ Comment("commented"),
+ Text("textual"),
+ PCData("data"),
+
+ )
+
+ nodes.sorted shouldBe Seq(
+ ,
+ Text("textual"),
+ PCData("data"),
+ Comment("commented")
+ )
+ }
+
+ it should "order Elems firstly by label" in {
+ val nodes = Seq[Node](
+ ,
+
+ )
+
+ nodes.sorted shouldBe Seq(
+ ,
+
+ )
+ }
+
+ it should "order Elems secondly by attribute count" in {
+ val nodes = Seq[Node](
+ ,
+ ,
+
+ )
+
+ nodes.sorted shouldBe Seq(
+ ,
+ ,
+
+ )
+ }
+
+ it should "order Elems thirdly by attribute names" in {
+ val nodes = Seq[Node](
+ ,
+ ,
+
+ )
+
+ nodes.sorted shouldBe Seq(
+ ,
+ ,
+
+ )
+ }
+
+ it should "order Elems finally by attribute values" in {
+ val nodes = Seq[Node](
+ ,
+ ,
+
+ )
+
+ nodes.sorted shouldBe Seq(
+ ,
+ ,
+
+ )
+ }
+
+ it should "order Text nodes" in {
+ val nodes = Seq[Node](Text("banana"), Text("apple"), Text("cherry"))
+ nodes.sorted shouldBe Seq(Text("apple"), Text("banana"), Text("cherry"))
+ }
+
+ it should "order PCData nodes" in {
+ val nodes = Seq[Node](PCData("banana"), PCData("apple"), PCData("cherry"))
+ nodes.sorted shouldBe Seq(PCData("apple"), PCData("banana"), PCData("cherry"))
+ }
+
+ it should "order Comment nodes" in {
+ val nodes = Seq[Node](Comment("banana"), Comment("apple"), Comment("cherry"))
+ nodes.sorted shouldBe Seq(Comment("apple"), Comment("banana"), Comment("cherry"))
+ }
+}
diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala
index e77f58a..d498b9d 100644
--- a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala
+++ b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala
@@ -131,6 +131,13 @@ class XmlCompareSpec extends FlatSpec with Matchers {
)
}
+ it should "not-match XML with children in different order with attributes" in {
+ XmlCompare.compare(
+ ,
+
+ ) shouldBe XmlDiffers("different value for attribute 'value'", "a", "b", Seq("test", "child"))
+ }
+
it should "not-match with multiple errors" in {
XmlCompare.compare(
text-1text-2,
@@ -142,41 +149,87 @@ class XmlCompareSpec extends FlatSpec with Matchers {
}
"compare without IgnoreNamespacePrefix" should "not-match different namespace prefix" in {
- XmlCompare.compare(, , Set.empty) shouldBe XmlDiffers(
- "different namespace prefix",
- "t",
- "e",
- Seq("t:test"))
+ XmlCompare.compare(
+ ,
+ ,
+ Set.empty
+ ) shouldBe XmlDiffers("different namespace prefix", "t", "e", Seq("t:test"))
}
it should "match same namespaces" in {
- XmlCompare.compare(, , Set.empty) shouldBe XmlEqual
+ XmlCompare.compare(
+ ,
+ ,
+ Set.empty
+ ) shouldBe XmlEqual
}
"compare with IgnoreNamespace" should "match different namespace prefix" in {
XmlCompare.compare(
,
,
- Set(IgnoreNamespace)) shouldBe XmlEqual
+ Set(IgnoreNamespace)
+ ) shouldBe XmlEqual
}
it should "match different namespace" in {
XmlCompare.compare(
,
,
- Set(IgnoreNamespace)) shouldBe XmlEqual
+ Set(IgnoreNamespace)
+ ) shouldBe XmlEqual
}
"compare with StrictAttributeOrder" should "match with same attributes" in {
- XmlCompare.compare(, , Set(StrictAttributeOrdering)) shouldBe XmlEqual
+ XmlCompare.compare(
+ ,
+ ,
+ Set(StrictAttributeOrdering)
+ ) shouldBe XmlEqual
}
it should "not-match with attributes in different order" in {
- XmlCompare.compare(, , Set(StrictAttributeOrdering)) shouldBe XmlDiffers(
+ XmlCompare.compare(
+ ,
+ ,
+ Set(StrictAttributeOrdering)
+ ) shouldBe XmlDiffers(
"different attribute ordering",
Seq("first", "second"),
Seq("second", "first"),
Seq("test")
)
}
+
+ "compare with IgnoreChildOrder" should "match XML with children in same order" in {
+ XmlCompare.compare(
+ ,
+ ,
+ Set(IgnoreChildOrder)
+ ) shouldBe XmlEqual
+ }
+
+ it should "match XML with children in different order" in {
+ XmlCompare.compare(
+ ,
+ ,
+ Set(IgnoreChildOrder)
+ ) shouldBe XmlEqual
+ }
+
+ it should "match XML with children in different order with attributes" in {
+ XmlCompare.compare(
+ ,
+ ,
+ Set(IgnoreChildOrder)
+ ) shouldBe XmlEqual
+ }
+
+ it should "match XML with children in different order with multiple attributes" in {
+ XmlCompare.compare(
+ ,
+ ,
+ Set(IgnoreChildOrder)
+ ) shouldBe XmlEqual
+ }
}