Skip to content

Commit 2a91ee9

Browse files
author
Andrey Onistchuk
committed
add some test
rewrite XmlPath to more functional implementation
1 parent 02a7ac0 commit 2a91ee9

File tree

7 files changed

+210
-111
lines changed

7 files changed

+210
-111
lines changed

β€Žproject/ScalaXmlDiffBuild.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ object ScalaXmlDiffBuild extends Build {
4848

4949
object BuildSettings {
5050

51-
val ver = "1.1.2"
51+
val ver = "1.1.3"
5252

5353
lazy val settings = Defaults.coreDefaultSettings ++ Seq(
5454
version := ver,

β€Žsrc/main/scala/com/github/andyglow/xml/diff/XmlComparator.scala

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package com.github.andyglow.xml.diff
1919

20+
import XmlDiff._
21+
2022
case class XmlComparisonContext(path: List[xml.Node] = Nil) {
2123
def append(node: xml.Node): XmlComparisonContext = this.copy(path = node :: path)
2224
}
@@ -44,41 +46,39 @@ class XmlComparator(
4446
private def compareChildren(
4547
context: XmlComparisonContext,
4648
expected: List[xml.Node],
47-
actual: List[xml.Node]): XmlDiff = {
49+
actual: List[xml.Node]): XmlDiffResult = {
4850

4951
expected match {
5052
case e :: tail =>
5153
e match {
5254
case e: xml.Elem if !shouldSkip(context, e) => findMatchingNode(context, tail, actual, e)
53-
case xml.Text(_t) if _t.trim.isEmpty => NoDiff // ???
55+
case xml.Text(_t) if _t.trim.isEmpty => XmlEqual
5456
case _ => compare(e, actual.head, context)
5557
}
56-
case Nil => NoDiff
58+
case Nil => XmlEqual
5759
}
5860
}
5961

6062
private def findMatchingNode(
6163
context: XmlComparisonContext,
6264
expected: List[xml.Node],
6365
actual: List[xml.Node],
64-
e: xml.Node): XmlDiff = {
66+
e: xml.Node): XmlDiffResult = {
6567

6668
val comparison = actual.filter {
6769
case a: xml.Node => isNodeNamesEqual(context, e, a)
6870
case _ => false
69-
} map { n =>
70-
val diff = compare(e, n, context)
71-
(n, diff)
72-
}
71+
} map { n => (n, compare(e, n, context)) }
7372

7473
if (comparison.isEmpty)
75-
NodeNotFound(context.path, e)
74+
XmlDifferent(AbsentNode(context.path, e))
7675
else {
77-
val sortOfSimilar = comparison.map(_._2)
78-
val matched = sortOfSimilar.find(_ == NoDiff)
79-
matched match {
80-
case Some(NoDiff) => compareChildren(context, expected, actual.dropWhile(n=>comparison.exists(_._1 == n)))
81-
case _ => ChildrenDiff(context.path, e, sortOfSimilar)
76+
def diffs = comparison collect { case (_, XmlDifferent(diff)) => diff }
77+
val hasEquals = comparison collectFirst { case (_, XmlEqual) => true } getOrElse false
78+
if (hasEquals) {
79+
compareChildren(context, expected, actual.dropWhile(n => comparison.exists { case (node, _) => node == n }))
80+
} else {
81+
XmlDifferent(ChildrenDiff(context.path, e, diffs))
8282
}
8383
}
8484
}
@@ -106,37 +106,37 @@ class XmlComparator(
106106
}
107107
}
108108

109-
def compare(expected: xml.Node, actual: xml.Node, context: XmlComparisonContext = XmlComparisonContext()): XmlDiff = {
109+
def compare(expected: xml.Node, actual: xml.Node, context: XmlComparisonContext = XmlComparisonContext()): XmlDiffResult = {
110110
(expected, actual) match {
111111

112112
case (xml.Comment(_), _) | (_, xml.Comment(_)) =>
113-
NoDiff
113+
XmlEqual
114114

115115
case (xml.Text(t1), xml.Text(t2)) =>
116116
if (ignoreTextDiffs || t1.trim == t2.trim)
117-
NoDiff
117+
XmlEqual
118118
else
119-
NodeDiff(context.path, expected, actual)
119+
XmlDifferent(NodeDiff(context.path, expected, actual))
120120

121121
case (e1: xml.Elem, e2: xml.Elem) =>
122122
val next = context.append(e1)
123123

124124
if (shouldSkip(next, e1))
125-
NoDiff
125+
XmlEqual
126126
else if (isNodeNamesEqual(next, e1, e2)) {
127127
if (isAttrsEqual(e1, e2))
128128
compareChildren(next, e1.child.toList, e2.child.toList)
129129
else
130-
AttributesDiff(next.path, e1.attributes, e2.attributes)
130+
XmlDifferent(AttributesDiff(next.path, e1.attributes, e2.attributes))
131131
} else {
132-
NodeDiff(next.path, e1, e2)
132+
XmlDifferent(NodeDiff(next.path, e1, e2))
133133
}
134134

135135
case (e1: xml.Node, e2: xml.Node) =>
136136
if (ignoreTextDiffs || e1.text == e2.text)
137-
NoDiff
137+
XmlEqual
138138
else
139-
NodeDiff(context.path, e1, e2)
139+
XmlDifferent(NodeDiff(context.path, e1, e2))
140140

141141
}
142142
}

β€Žsrc/main/scala/com/github/andyglow/xml/diff/XmlDiff.scala

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
*/
1818
package com.github.andyglow.xml.diff
1919

20-
sealed trait XmlDiff
20+
sealed trait XmlDiffResult
2121

22-
case object NoDiff extends XmlDiff
22+
case object XmlEqual extends XmlDiffResult
23+
case class XmlDifferent(diff: XmlDiff) extends XmlDiffResult
2324

24-
sealed trait TheDiff extends XmlDiff {
25+
sealed trait XmlDiff {
2526
def path: List[xml.Node]
2627
}
2728

@@ -31,37 +32,40 @@ private[diff] object XmlDiff {
3132
def name: String = n.nameToString(new StringBuilder).toString()
3233
}
3334

34-
}
35+
case class RedundantNode(path: List[xml.Node], node: xml.Node) extends XmlDiff {
36+
override def toString = s"""RedundantNode(
37+
| $node
38+
|)""".stripMargin
39+
}
3540

36-
case class IllegalNodeFound(path: List[xml.Node], node: xml.Node) extends TheDiff {
37-
override def toString = s"""IllegalNodeFound(
38-
| ${node}
39-
|)""".stripMargin
40-
}
41+
case class AbsentNode(path: List[xml.Node], node: xml.Node) extends XmlDiff {
42+
override def toString = s"""AbsentNode(
43+
| $node
44+
|)""".stripMargin
45+
}
4146

42-
case class NodeNotFound(path: List[xml.Node], node: xml.Node) extends TheDiff {
43-
override def toString = s"""NodeNotFound(
44-
| ${node}
45-
|)""".stripMargin
46-
}
47+
case class NodeDiff(path: List[xml.Node], expected: xml.Node, actual: xml.Node) extends XmlDiff {
48+
override def toString = s"""NodeDiff(
49+
| Expected: ${expected}
50+
| Actual: ${actual}
51+
|)""".stripMargin
52+
}
4753

48-
case class NodeDiff(path: List[xml.Node], expected: xml.Node, actual: xml.Node) extends TheDiff {
49-
override def toString = s"""NodeDiff(
50-
| Expected: ${expected}
51-
| Found: ${actual}
52-
|)""".stripMargin
53-
}
54+
case class AttributesDiff(path: List[xml.Node], expected: xml.MetaData, actual: xml.MetaData) extends XmlDiff {
55+
override def toString = s"""AttributesDiff(
56+
| Expected: ${expected.asAttrMap}
57+
| Actual: ${actual.asAttrMap}
58+
|)""".stripMargin
59+
}
5460

55-
case class AttributesDiff(path: List[xml.Node], expected: xml.MetaData, actual: xml.MetaData) extends TheDiff {
56-
override def toString = s"""AttributesDiff(
57-
| Expected: ${expected.asAttrMap}
58-
| Found: ${actual.asAttrMap}
59-
|)""".stripMargin
60-
}
61+
case class ChildrenDiff(path: List[xml.Node], element: xml.Node, list: List[XmlDiff]) extends XmlDiff {
62+
override def toString = {
63+
val wrongElementsReport = list.mkString(",\n").lines.map(" " + _).mkString("\n")
64+
s"""ChildrenDiff(
65+
| None of the elements found fully matched $element
66+
|$wrongElementsReport
67+
|)""".stripMargin
68+
}
69+
}
6170

62-
case class ChildrenDiff(path: List[xml.Node], element: xml.Node, list: List[XmlDiff]) extends TheDiff {
63-
override def toString = s"""ChildrenDiff(
64-
| None of the elements found fully matched ${element}
65-
|${list.mkString(",\n").lines.map(" " + _).mkString("\n")}
66-
|)""".stripMargin
67-
}
71+
}

β€Žsrc/main/scala/com/github/andyglow/xml/diff/XmlPath.scala

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,74 @@
1717
*/
1818
package com.github.andyglow.xml.diff
1919

20-
case class XmlPath(elems: List[String]) {
21-
22-
/** Returns true if this XPath matches the given path. */
23-
def matches(other: List[String]): Boolean = {
24-
var xs = elems
25-
var ys = other
26-
27-
while (ys != Nil && xs != Nil) {
28-
if (xs.head == XmlPath.WILDCARD) {
29-
// skip any number of non-matching elements from 'other'
30-
while (ys != Nil && xs.tail.head != ys.head) ys = ys.tail
31-
xs = xs.tail
32-
} else if (xs.head == ys.head) {
33-
xs = xs.tail
34-
ys = ys.tail
35-
} else
36-
return false
37-
}
38-
xs == Nil
39-
}
40-
41-
override def toString = elems.mkString("", "/", "")
20+
sealed trait XmlPath {
21+
def head: XmlPath.NameMatcher
22+
def tail: XmlPath
23+
def isEmpty: Boolean
24+
def matches(path: List[String]): Boolean
25+
def ::(head: XmlPath.NameMatcher) = XmlPath.::(head, this)
4226
}
4327

4428
object XmlPath {
4529

46-
def apply(path: String): XmlPath = XmlPath(parse(path))
30+
sealed trait NameMatcher extends (String => Boolean)
31+
object NameMatcher {
32+
case object Wildcard extends NameMatcher {
33+
def apply(x: String): Boolean = true
34+
override def toString: String = "*"
35+
}
36+
case class Text(name: String) extends NameMatcher {
37+
def apply(x: String): Boolean = x == name
38+
override def toString: String = name
39+
}
40+
def apply(text: String): NameMatcher = text.trim match {
41+
case "*" => Wildcard
42+
case x => Text(x)
43+
}
44+
}
4745

48-
val WILDCARD = "*"
46+
case object Nil extends XmlPath {
47+
override def isEmpty: Boolean = true
48+
override def head: NameMatcher = throw new NoSuchElementException("head of empty xml path")
49+
override def tail: XmlPath = throw new UnsupportedOperationException("tail of empty xml path")
50+
override def equals(that: Any) = that.isInstanceOf[Nil.type]
51+
override def matches(path: List[String]): Boolean = path.isEmpty
52+
override def toString: String = ""
53+
}
4954

50-
private def parse(path: String): List[String] = path.split('/')
51-
.map(_.trim)
52-
.filter(!_.isEmpty)
53-
// remove duplicated *
54-
.foldRight(Nil: List[String]) {(token, list) => list match {
55-
case h :: t if h == WILDCARD && token == WILDCARD => list
56-
case _ => token :: list
57-
}}
55+
case class ::(head: NameMatcher, tail: XmlPath) extends XmlPath {
56+
override def toString: String = tail.toString match {
57+
case "" => head.toString()
58+
case x => head + "/" + x
59+
}
60+
override def isEmpty: Boolean = false
61+
override def matches(path: List[String]): Boolean = {
62+
def tokenMatches(thatHead: String): (Boolean, List[String]) = head match {
63+
case NameMatcher.Wildcard if !tail.isEmpty => (true, path.dropWhile(!tail.head(_)))
64+
case NameMatcher.Wildcard => (true, scala.Nil)
65+
case NameMatcher.Text(thisHead) => (thatHead equals thisHead, path.tail)
66+
}
5867

59-
}
68+
val matches = for {
69+
thatHead <- path.headOption
70+
(headMatches, rest) = tokenMatches(thatHead) if headMatches
71+
result = tail.matches(rest)
72+
} yield result
73+
74+
matches getOrElse false
75+
}
76+
}
77+
78+
def apply(path: String): XmlPath = parse(path)
79+
80+
private def parse(path: String): XmlPath =
81+
path
82+
.split('/')
83+
.map(_.trim)
84+
.filter(!_.isEmpty)
85+
.foldRight(Nil: XmlPath) { (token, list) => list match { // remove duplicated *
86+
case h :: t if h == NameMatcher.Wildcard && token == "*" => list
87+
case _ => NameMatcher(token) :: list
88+
}}
89+
90+
}

β€Žsrc/main/scala/com/github/andyglow/xml/diff/package.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package com.github.andyglow.xml
2020
package object diff {
2121

2222
implicit class XmlOps(val x: xml.Elem) extends AnyVal {
23-
def compareTo(o: xml.Elem): XmlDiff = XmlComparator().compare(x, o)
23+
def compareTo(o: xml.Elem): XmlDiffResult = XmlComparator().compare(x, o)
2424
}
2525

2626
}

0 commit comments

Comments
 (0)