Skip to content
Browse files

Adding Zipper axes support.

  • Loading branch information...
1 parent 5ec69c4 commit 02294159d8ec2b0ea671914c5e5649f3577c7189 @ncreep committed Dec 11, 2011
View
71 src/main/scala/com/codecommit/antixml/ZipperAxes.scala
@@ -0,0 +1,71 @@
+package com.codecommit.antixml
+import scala.annotation.tailrec
+
+/**
+ * Wraps [[com.codecommit.antixml.Zipper]] instances with some XPath like axes.
+ *
+ * Note1: the axes are applied to each node in a zipper individually and the result
+ * is a new zipper with the nodes concatenated and sorted lexicographically by
+ * location (removing any duplicate locations).
+ *
+ * Note2: the axes are calculated using holes in the zipper, hence for a modified
+ * zipper some nodes may be multiplied or elided.
+ */
+class ZipperAxes(zipper: Zipper[Node]) {
+ /** Returns the direct parent of a node. */
+ def directParent = {
+ zipper shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList))
+ }
+
+ /** Returns the ancestors of a node. */
+ def ancestor = transFuncToShift(_.shiftUp, false)
+
+ /** Returns the ancestors of a node including itself. */
+ def ancestorOrSelf = transFuncToShift(_.shiftUp, true)
+
+ /** Returns the following siblings of a node. */
+ def followingSibling = transFuncToShift(_.shiftRight, false)
+
+ /** Returns the following siblings of a node including itself. */
+ def followingSiblingOrSelf = transFuncToShift(_.shiftRight, true)
+
+ /** Returns the preceding siblings of a node. */
+ def precedingSibling = transFuncToShift(_.shiftLeft, false)
+
+ /** Returns the preceding siblings of a node including itself. */
+ def precedingSiblingOrSelf = transFuncToShift(_.shiftLeft, true)
+
+ /** Takes a path transformer function and converts it to a shifting function which is applied until
+ * the transformer return `None`.
+ * @param appendSource True if the initial path should be part of the result.
+ */
+ private def transFuncToShift(func: PathTransformer => ZipperPath => Option[ZipperPath], withSource: Boolean) = {
+ zipper shiftHoles { g =>
+ val pathToOpt = func(PathTransformer(g))
+
+ @tailrec
+ def traverse(path: ZipperPath, res: List[ZipperPath]): List[ZipperPath] = {
+ val opt = pathToOpt(path)
+ opt match {
+ case None => res
+ case Some(p) => traverse(p, p :: res)
+ }
+ }
+
+ val shiftFunc = (p: ZipperPath) => {
+ val init =
+ if (withSource) List(p)
+ else List()
+ traverse(p, init)
+ }
+ shiftFunc
+ }
+ }
+
+}
+
+object ZipperAxes {
+ /** Pimps a plain zipper to have axes selection methods.
+ * TODO move to package object? */
+ implicit def zipperToAxes(zipper: Zipper[Node]) = new ZipperAxes(zipper)
+}
View
154 src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala
@@ -0,0 +1,154 @@
+package com.codecommit.antixml
+
+import org.specs2.mutable._
+import XML._
+import ZipperAxes._
+import scala.collection.immutable.SortedSet
+
+class ZipperAxesSpecs extends SpecificationWithJUnit {
+ val bookstore = resource("bookstore.xml").toGroup
+
+ "Axes" should {
+ "preserve past modifications" in {
+ val zipper = (bookstore \\ 'author).updated(3, Text("foo"))
+ val res = zipper.unselect
+
+ zipper.directParent.unselect mustEqual res
+ zipper.ancestor.unselect mustEqual res
+ zipper.ancestorOrSelf.unselect mustEqual res
+ zipper.followingSibling.unselect mustEqual res
+ zipper.followingSiblingOrSelf.unselect mustEqual res
+ zipper.precedingSibling.unselect mustEqual res
+ zipper.precedingSiblingOrSelf.unselect mustEqual res
+ }
+
+ "fail on broken zippers" in {
+ bookstore.toZipper.directParent.unselect must throwA[RuntimeException]
+ bookstore.toZipper.ancestor.unselect must throwA[RuntimeException]
+ bookstore.toZipper.ancestorOrSelf.unselect must throwA[RuntimeException]
+ bookstore.toZipper.followingSibling.unselect must throwA[RuntimeException]
+ bookstore.toZipper.followingSiblingOrSelf.unselect must throwA[RuntimeException]
+ bookstore.toZipper.precedingSibling.unselect must throwA[RuntimeException]
+ bookstore.toZipper.precedingSiblingOrSelf.unselect must throwA[RuntimeException]
+ }
+ }
+
+ "Direct parent axes" should {
+ "be empty for root" in {
+ val res = bookstore select 'bookstore directParent
+
+ res mustEqual Group()
+ res.unselect mustEqual bookstore
+ }
+
+ "return the direct parents of nodes" in {
+ val res = bookstore \\ 'author directParent;
+
+ res mustEqual (bookstore \\ 'book)
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Ancestor axes" should {
+ "be empty for root" in {
+ val res = bookstore select 'bookstore ancestor
+
+ res mustEqual Group()
+ res.unselect mustEqual bookstore
+ }
+
+ "return all ancestors of nodes" in {
+ val res = bookstore \\ 'author ancestor;
+
+ res mustEqual (bookstore ++ bookstore \\ 'book)
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Ancestor or self axes" should {
+ "return self for root" in {
+ val res = bookstore select 'bookstore ancestorOrSelf
+
+ res mustEqual bookstore
+ res.unselect mustEqual bookstore
+ }
+
+ "return all ancestors and self of nodes" in {
+ val books = bookstore \\ 'book
+ val authors = bookstore \\ 'author
+
+ val res = authors ancestorOrSelf;
+
+
+ res mustEqual (bookstore :+ books(0) :+ authors(0) :+ books(1) :+ authors(1) :+ books(2) :+ authors(2) :+ authors(3))
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Following sibling axes" should {
+ "return nothing on rightmost node" in {
+ val res = bookstore \\ 'author followingSibling
+
+ res mustEqual Group((bookstore \\ 'author).apply(3))
+ res.unselect mustEqual bookstore
+ }
+
+ "return all following siblings of nodes" in {
+ val res = bookstore \\ 'title followingSibling
+
+ res mustEqual bookstore \\ 'author
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Following sibling or self axes" should {
+ "return self for rightmost nodes" in {
+ val res = bookstore \\ 'author followingSiblingOrSelf
+
+ res mustEqual (bookstore \\ 'author)
+ res.unselect mustEqual bookstore
+ }
+
+ "return all following siblings and self of nodes" in {
+ val res = bookstore \\ 'title followingSiblingOrSelf
+
+ res mustEqual bookstore \ 'book \ *
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Preceding sibling axes" should {
+ "return nothing on leftmost node" in {
+ val res = bookstore \\ 'title precedingSibling
+
+ res mustEqual Group()
+ res.unselect mustEqual bookstore
+ }
+
+ "return all preceding siblings of nodes" in {
+ val res = bookstore \\ 'author precedingSibling
+
+ res mustEqual (bookstore \\ 'title) :+ (bookstore \\ 'author).apply(2)
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ "Preceding sibling or self axes" should {
+ "return self for leftmost nodes" in {
+ val res = bookstore \\ 'title precedingSiblingOrSelf
+
+ res mustEqual (bookstore \\ 'title)
+ res.unselect mustEqual bookstore
+ }
+
+ "return all preceding siblings and self of nodes" in {
+ val res = bookstore \\ 'author precedingSiblingOrSelf
+
+ res mustEqual bookstore \ 'book \ *
+ res.unselect mustEqual bookstore
+ }
+ }
+
+ def resource(filename: String) =
+ XML fromSource (scala.io.Source fromURL (getClass getResource ("/" + filename)))
+}

0 comments on commit 0229415

Please sign in to comment.
Something went wrong with that request. Please try again.