├── project
├── build.properties
└── Dependencies.scala
├── .gitignore
└── src
├── test
└── scala
│ └── net
│ └── michalsitko
│ ├── utils
│ ├── CogenInstances.scala
│ ├── XmlFragments.scala
│ └── ArbitraryInstances.scala
│ ├── laws
│ └── LawsSpec.scala
│ └── XmlOpticsSpec.scala
└── main
└── scala
└── net
└── michalsitko
├── naive
└── XmlSupport.scala
└── optics
└── Optics.scala
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.15
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /RUNNING_PID
2 | /logs/
3 | /project/*-shim.sbt
4 | /project/project/
5 | /project/target/
6 | /target/
7 | /.idea
--------------------------------------------------------------------------------
/project/Dependencies.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | object Dependencies {
4 | lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1"
5 | }
6 |
--------------------------------------------------------------------------------
/src/test/scala/net/michalsitko/utils/CogenInstances.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.utils
2 |
3 | import cats.data.NonEmptyList
4 | import org.scalacheck.Cogen
5 |
6 | import scala.xml.{Elem, NodeSeq}
7 |
8 | trait CogenInstances {
9 | implicit val nodeSeqCogen = Cogen[NodeSeq]((_ : NodeSeq).hashCode().toLong)
10 | implicit val nonEmptyListOfElemCogen = Cogen[NonEmptyList[Elem]]((_: NonEmptyList[Elem]).hashCode().toLong)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/scala/net/michalsitko/naive/XmlSupport.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.naive
2 |
3 | import scala.xml.{Elem, Node, NodeSeq}
4 |
5 | // whole "naive" packages should be eventually abandoned
6 | // it may be useful just for documentary purposes - to show motivation behind the project
7 | // it contains some naive helper function that try to overcome some scala-xml API quirks
8 | trait XmlSupport {
9 | def deeper(toReplace: String)(fn: PartialFunction[Node, NodeSeq]): PartialFunction[Node, NodeSeq] = {
10 | case elem: Elem if (elem.label == toReplace) =>
11 | elem.copy(child = elem.child.flatMap(fn))
12 | case e =>
13 | e
14 | }
15 |
16 | def update(toReplace: String)(fn: PartialFunction[Node, Node]): PartialFunction[Node, NodeSeq] = {
17 | case elem: Elem if (elem.label == toReplace) =>
18 | elem.map(fn)
19 | case e => e
20 | }
21 |
22 | def extract(parent: NodeSeq, fieldName: String): Option[String] = {
23 | (parent \ fieldName).headOption.map(_.text)
24 | }
25 |
26 | def getAttr(parent: NodeSeq, attrName: String): Option[String] = {
27 | parent.headOption.flatMap(_.headOption).flatMap(_.attribute(attrName)).map(_.text)
28 | }
29 | }
30 |
31 | object XmlSupport extends XmlSupport
32 |
--------------------------------------------------------------------------------
/src/test/scala/net/michalsitko/laws/LawsSpec.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.laws
2 |
3 | import cats.data.NonEmptyList
4 | import monocle.internal.IsEq
5 | import monocle.law.discipline.{LensTests, OptionalTests}
6 | import monocle.law.{LensLaws, PrismLaws}
7 | import net.michalsitko.optics.Optics
8 | import net.michalsitko.utils.{ArbitraryInstances, CogenInstances, XmlFragments}
9 | import org.scalacheck.Arbitrary
10 | import org.scalactic.anyvals.PosZInt
11 | import org.scalatest.matchers.{MatchResult, Matcher}
12 | import org.scalatest.prop.Checkers
13 | import org.scalatest.{FlatSpec, Matchers, WordSpec}
14 | import org.typelevel.discipline.Laws
15 |
16 | import scala.xml._
17 | import scala.xml.Utility
18 | import scalaz.Equal
19 |
20 | class LawsSpec extends OpticsSpec with Matchers with ArbitraryInstances with CogenInstances {
21 | // implicit val nodeEqualInstance: Equal[Node] = (n1: Node, n2: Node) => n1 == n2
22 | // implicit val nodeSeqEqualInstance: Equal[NodeSeq] = (n1: NodeSeq, n2: NodeSeq) => n1 == n2
23 |
24 | implicit val elemEqual: Equal[Elem] = (e1: Elem, e2: Elem) => {
25 | // TODO: not sure it's proper. Better than `e1 == e2` which has a problem with whitespaces
26 | Utility.trim(e1) == Utility.trim(e2)
27 | }
28 | implicit val nonEmptyListOfElemsEqual: Equal[NonEmptyList[Elem]] = {
29 | (l1: NonEmptyList[Elem], l2: NonEmptyList[Elem]) =>
30 | val elemEq = implicitly[Equal[Elem]]
31 | val (list1, list2) = (l1.toList, l2.toList)
32 | list1.size == list2.size && list1.zip(list2).forall(t => elemEq.equal(t._1, t._2))
33 | }
34 |
35 |
36 | val elem: Elem = XML.loadString(XmlFragments.simpleAsString)
37 |
38 | // implicit val arbNode = Arbitrary[Node](elemWithLabelOccurance(4, "abc"))
39 | // implicit val arbNodeSeq = Arbitrary(nodeSeq(2))
40 | implicit val arbElem = Arbitrary[Elem](elemWithLabelOccurance(2, "abc"))
41 | implicit val arbElems = arbNonEmptyListOfElems(arbElem)
42 |
43 | // val nodeLens = LensTests(Optics.nodeLens("abc"))
44 | val elemTest = OptionalTests(Optics.elem("abc"))
45 |
46 | checkLaws("elem Optional", elemTest)
47 |
48 | }
49 |
50 | trait OpticsSpec extends FlatSpec {
51 | def checkLaws(name: String, ruleSet: Laws#RuleSet, maxSize: Int = 100): Unit = {
52 | val configParams = List(Checkers.MinSuccessful(15), Checkers.SizeRange(PosZInt.from(maxSize).get))
53 |
54 | ruleSet.all.properties.zipWithIndex.foreach {
55 | case ((id, prop), 0) => name should s"obey $id" in Checkers.check(prop, configParams:_*)
56 | case ((id, prop), _) => it should s"obey $id" in Checkers.check(prop, configParams:_*)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/test/scala/net/michalsitko/utils/XmlFragments.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.utils
2 |
3 | trait XmlFragments {
4 | val xmlAsString =
5 | """
6 | |
7 | |
8 | |
9 | |
10 | |
11 | | item1
12 | | item2
13 | |
14 | |
15 | | item1
16 | | item2
17 | | item3
18 | | item1
19 | |
20 | |
21 | | item1
22 | | item2
23 | | item3
24 | | item1
25 | |
26 | |
27 | | summary
28 | |
29 | |
30 | |
31 | """.stripMargin
32 |
33 | val simpleAsString =
34 | """
35 | |
36 | |
37 | | item1
38 | | item2
39 | |
40 | |
41 | | item1
42 | | item2
43 | |
44 | |
45 | | item1
46 | | item2
47 | | item3
48 | |
49 | | summary
50 | |
51 | """.stripMargin
52 |
53 | val verySimpleString =
54 | """
55 | |
56 | |
57 | | item1
58 | | item2
59 | |
60 | |
61 | | item1
62 | | item2
63 | | item3
64 | |
65 | | summary
66 | |
67 | """.stripMargin
68 |
69 | // TODO: to remove
70 | val xmlString1 =
71 | """
72 | |
73 | |
74 | | item1
75 | |
76 | |
77 | """.stripMargin
78 |
79 | val xmlString2 =
80 | """
81 | |
82 | |
83 | | item1
84 | |
85 | |
86 | """.stripMargin
87 |
88 | val asLiteral =
89 |
90 |
91 |
92 |
93 |
94 | item1
95 | item2
96 |
97 |
98 | item1
99 | item2
100 | item3
101 |
102 |
103 | summary
104 |
105 |
106 |
107 | }
108 |
109 | object XmlFragments extends XmlFragments
110 |
--------------------------------------------------------------------------------
/src/main/scala/net/michalsitko/optics/Optics.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.optics
2 |
3 | import cats.data.NonEmptyList
4 | import monocle.{Lens, Optional, Prism, Traversal}
5 |
6 | import scala.xml.{Elem, Node, NodeSeq}
7 | import scalaz.Applicative
8 | import scalaz.std.list._
9 |
10 | // a messy attempt to implement Optics over scala-xml
11 | trait Optics {
12 |
13 | def elem(label: String): Optional[Elem, NonEmptyList[Elem]] = Optional[Elem, NonEmptyList[Elem]]{
14 | parent =>
15 | val nodes = parent \ label
16 | val elems = nodes.foldRight(List.empty[Elem]) { (current, acc) =>
17 | current match {
18 | case elem: Elem => elem :: acc
19 | }
20 | }
21 | NonEmptyList.fromList(elems)
22 | }{ newElems => parent =>
23 | val it = newElems.toList.toIterator
24 | val children = parent.child.flatMap {
25 | case el: Elem if el.label == label =>
26 | if(it.hasNext){
27 | Some(it.next())
28 | } else {
29 | None
30 | }
31 | case el =>
32 | Some(el)
33 | }
34 | parent.copy(child = children)
35 | }
36 |
37 | def nodeLens(fieldName: String): Lens[Node, NodeSeq] = Lens.apply[Node, NodeSeq](
38 | elem => elem \ fieldName
39 | ){newNodeSeq => rootNode =>
40 | val it = newNodeSeq.toIterator
41 | val children = rootNode.child.flatMap {
42 | case el: Elem if el.label == fieldName =>
43 | if(it.hasNext){
44 | Some(it.next())
45 | } else {
46 | None
47 | }
48 | case el =>
49 | Some(el)
50 | }
51 | rootNode.asInstanceOf[Elem].copy(child = children)
52 | }
53 |
54 | def nodeLens2(fieldName: String): Lens[NodeSeq, NodeSeq] = Lens.apply[NodeSeq, NodeSeq](
55 | elem => elem \ fieldName
56 | ){newNodeSeq => rootNode =>
57 | val it = newNodeSeq.toIterator
58 | val r = rootNode.map {
59 | case e: Elem =>
60 | val children = e.child.flatMap {
61 | case el: Elem if el.label == fieldName =>
62 | if(it.hasNext){
63 | Some(it.next)
64 | } else {
65 | None
66 | }
67 | case el =>
68 | Some(el)
69 | }
70 | e.copy(child = children)
71 | case e => e
72 | }
73 | NodeSeq.fromSeq(r)
74 | }
75 |
76 | val elemPrism: Prism[Node, Elem] = Prism[Node, Elem](node => node match {
77 | case e: Elem => Some(e)
78 | case _ => None
79 | })(el => el)
80 |
81 | val each = new Traversal[NodeSeq, Node]{
82 | final def modifyF[F[_]](f: Node => F[Node])(from: NodeSeq)(implicit F: Applicative[F]): F[NodeSeq] = {
83 | val mapped = from.theSeq.map(f).toList
84 | F.map(F.sequence(mapped))(nodes => NodeSeq.fromSeq(nodes))
85 | }
86 | }
87 |
88 | }
89 |
90 | object Optics extends Optics
91 |
--------------------------------------------------------------------------------
/src/test/scala/net/michalsitko/utils/ArbitraryInstances.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko.utils
2 |
3 | import cats.data.NonEmptyList
4 | import org.scalacheck.{Arbitrary, Gen}
5 |
6 | import scala.xml._
7 | import scala.collection.JavaConverters._
8 |
9 | trait ArbitraryInstances {
10 | def arbitraryNode(depth: Int): Arbitrary[Node] = ???
11 |
12 | def elemWithLabelOccurance(depth: Int, label: String): Gen[Elem] = {
13 | elemOfDepth(depth, Some(label))
14 | }
15 |
16 | def nodeSeq(depth: Int): Gen[NodeSeq] = {
17 | Gen.choose(0, 2).flatMap(n => Gen.listOfN(n, elemOfDepth(depth, None))).map(NodeSeq.fromSeq(_))
18 | }
19 |
20 | def arbNonEmptyListOfElems(arbElem: Arbitrary[Elem]): Arbitrary[NonEmptyList[Elem]] =
21 | Arbitrary(arbElem.arbitrary.flatMap(Gen.nonEmptyListOf(_).map(NonEmptyList.fromListUnsafe(_))))
22 |
23 | private val maxAttrsLength = 2
24 | private val maxDirectChilden = 3
25 |
26 | // TODO: Not sure if retryUntil is the way to go
27 | private val alphanumGen: Gen[String] = Gen.alphaStr.map(_.take(9) + "a")
28 |
29 | private val attrGen = for {
30 | attrName <- alphanumGen
31 | attrVal <- alphanumGen
32 | } yield new UnprefixedAttribute(attrName, new Text(attrVal), Null)
33 |
34 | private val attrsGen = for {
35 | length <- Gen.choose(0, maxAttrsLength)
36 | attributes <- Gen.listOfN(length, attrGen)
37 | } yield attributes
38 |
39 | private val linkedAttrs: Gen[MetaData] = attrsGen.map { listOfAttrs =>
40 | listOfAttrs.foldLeft[MetaData](Null){ (prev, current) =>
41 | new UnprefixedAttribute(current.key, current.value, prev)
42 | }
43 | }
44 |
45 | private def elemOfDepth(desiredDepth: Int, label: Option[String]): Gen[Elem] = {
46 | // TODO: non-tail recursion
47 | def loop(depth: Int): Gen[List[Elem]] = {
48 | if(depth == 0) {
49 | Gen.choose(1, maxDirectChilden).flatMap(n => Gen.listOfN(n, leafElem(List(Text("Hello XML")), label)))
50 | } else {
51 | val usedLabel = if(depth - 1 == desiredDepth) None else label
52 | for {
53 | newChildrenNumber <- Gen.choose(1, maxDirectChilden)
54 | children <- Gen.sequence(List.fill(newChildrenNumber)(loop(depth - 1)))
55 | currentNodes <- Gen.sequence(children.asScala.toList.map(ch => leafElem(ch, usedLabel)))
56 | currentNodesList <- currentNodes.asScala.toList
57 | } yield currentNodesList
58 | }
59 | }
60 |
61 | loop(desiredDepth - 1).map(_.head)
62 | }
63 |
64 | private def leafElem(children: List[Node], label: Option[String]): Gen[Elem] = {
65 | val labelGen = label match {
66 | case Some(desiredLabel) =>
67 | Gen.frequency[String]((9, alphanumGen), (1, Gen.const(desiredLabel)))
68 | case None =>
69 | alphanumGen
70 | }
71 |
72 | for {
73 | label <- labelGen
74 | attrs <- linkedAttrs
75 | } yield newElem(label, attrs, children)
76 | }
77 |
78 | private def newElem(label: String, attributes: MetaData, children: List[Node]) =
79 | Elem(prefix = null, label = label, attributes = attributes, scope = TopScope,
80 | minimizeEmpty = false, child = children:_*)
81 | }
82 |
83 | object ArbitraryInstances extends ArbitraryInstances
84 |
--------------------------------------------------------------------------------
/src/test/scala/net/michalsitko/XmlOpticsSpec.scala:
--------------------------------------------------------------------------------
1 | package net.michalsitko
2 |
3 | import monocle.{PTraversal, Traversal}
4 | import net.michalsitko.utils.XmlFragments
5 | import org.scalatest._
6 |
7 | import scala.xml._
8 |
9 | class XmlOpticsSpec extends WordSpec with Matchers with XmlFragments with Solutions {
10 | "naive solution" should {
11 | "work for text replacement" in {
12 | val simpleXml = XML.loadString(simpleAsString)
13 |
14 | val res = naive(simpleXml)
15 |
16 | val expectedXml = XML.loadString(ExpectedValues.simpleAsStringAfterTextReplacement)
17 | res.head should equal(expectedXml)
18 | }
19 | }
20 |
21 | "naiveXmlSupport" should {
22 | "work for text replacement" in {
23 | val simpleXml = XML.loadString(simpleAsString)
24 |
25 | val res = naiveXmlSupport(simpleXml)
26 |
27 | val expectedXml = XML.loadString(ExpectedValues.simpleAsStringAfterTextReplacement)
28 | res.head should equal(expectedXml)
29 | }
30 |
31 | "work for more complicated text replacement" in {
32 | val simpleXml = XML.loadString(xmlAsString)
33 |
34 | val res = naiveXmlSupport2(simpleXml)
35 |
36 | val expectedXml = XML.loadString(ExpectedValues.xmlAsStringAfterTextReplacement)
37 | res.head should equal(expectedXml)
38 | }
39 | }
40 |
41 | "Optics" should {
42 | "work for text replacement" in {
43 | val simpleXml = XML.loadString(simpleAsString)
44 |
45 | val res = withOptics(simpleXml)
46 |
47 | val expectedXml = XML.loadString(ExpectedValues.simpleAsStringAfterTextReplacement)
48 | res should equal(expectedXml)
49 | }
50 |
51 | "work for more complicated text replacement" in {
52 | val simpleXml = XML.loadString(xmlAsString)
53 |
54 | val res = withOptics2(simpleXml)
55 |
56 | val expectedXml = XML.loadString(ExpectedValues.xmlAsStringAfterTextReplacement)
57 | res should equal(expectedXml)
58 | }
59 |
60 | // TODO: to remove
61 | "tmp1" in {
62 | import net.michalsitko.optics.Optics._
63 |
64 | val simpleXml = XML.loadString(xmlString1)
65 |
66 | val focused = nodeLens("c1").composeLens(nodeLens2("f")).set(NodeSeq.fromSeq(Seq.empty))
67 | val res = focused(simpleXml)
68 |
69 | val expectedXml = XML.loadString("""
70 | |
71 | |
72 | |
73 | |
74 | |
75 | """.stripMargin)
76 |
77 | val trimmedEqual = scala.xml.Utility.trim(res) == scala.xml.Utility.trim(expectedXml)
78 |
79 | trimmedEqual should equal(true)
80 | }
81 |
82 | "tmp2" in {
83 | import net.michalsitko.optics.Optics._
84 |
85 | val simpleXml = XML.loadString(xmlString1)
86 |
87 | val newElem = txt
88 | val focused = nodeLens("c1").composeLens(nodeLens2("f")).set(NodeSeq.fromSeq(List(newElem)))
89 | val res = focused(simpleXml)
90 |
91 | val expectedXml = XML.loadString("""
92 | |
93 | |
94 | | txt
95 | |
96 | |
97 | """.stripMargin)
98 |
99 | val trimmedEqual = scala.xml.Utility.trim(res) == scala.xml.Utility.trim(expectedXml)
100 |
101 | trimmedEqual should equal(true)
102 | }
103 |
104 | "tmp3" in {
105 | import net.michalsitko.optics.Optics._
106 |
107 | val simpleXml = XML.loadString(xmlString2)
108 |
109 | val newElem = NodeSeq.fromSeq(List(txt))
110 | val focused = nodeLens("c1").composeLens(nodeLens2("f"))
111 | val res = focused.set(newElem)(simpleXml)
112 |
113 | val newF = focused.get(simpleXml)
114 |
115 | newF should equal(newElem)
116 | }
117 | }
118 |
119 | }
120 |
121 | trait Solutions {
122 | def naive(elem: Elem): NodeSeq = {
123 | elem.map {
124 | case aElem: Elem if (aElem.label == "a") =>
125 | aElem.copy(child = aElem.child.flatMap {
126 | case c1Elem: Elem if (c1Elem.label == "c1") =>
127 | c1Elem.copy(child = c1Elem.child.flatMap {
128 | case fElem: Elem if (fElem.label == "f") =>
129 | fElem.copy(child = List(Text("f replaced")))
130 | case el => el
131 | })
132 | case el => el
133 | })
134 | case el => el
135 | }
136 | }
137 |
138 | def naiveXmlSupport(elem: Elem): NodeSeq = {
139 | import net.michalsitko.naive.XmlSupport._
140 | elem.map {
141 | deeper("a")(deeper("c1")(update("f"){
142 | case elem: Elem => elem.copy(child = List(Text("f replaced")))
143 | }))
144 | }.head
145 | }
146 |
147 | def naiveXmlSupport2(elem: Elem): NodeSeq = {
148 | import net.michalsitko.naive.XmlSupport._
149 | elem.map {
150 | deeper("a")(deeper("b")(deeper("c")(deeper("d")(deeper("e2")(update("f"){
151 | case elem: Elem => elem.copy(child = List(Text("f replaced")))
152 | })))))
153 | }.head
154 | }
155 |
156 | def withOptics(element: Elem): NodeSeq = {
157 | import net.michalsitko.optics.Optics._
158 |
159 | val focused = (nodeLens("c1").composeLens(nodeLens2("f"))).composeTraversal(each.composePrism(elemPrism))
160 | focused.modify(_.copy(child = List(Text("f replaced"))))(element)
161 | }
162 |
163 | def withOptics2(element: Elem): NodeSeq = {
164 | import net.michalsitko.optics.Optics._
165 |
166 | val composed =
167 | nodeLens("b").composeLens(nodeLens2("c")).composeLens(nodeLens2("d")).composeLens(nodeLens2("e2")).composeLens(nodeLens2("f"))
168 | val focused = composed.composeTraversal(each.composePrism(elemPrism))
169 | focused.modify(_.copy(child = List(Text("f replaced"))))(element)
170 | }
171 | }
172 |
173 | object ExpectedValues {
174 | val simpleAsStringAfterTextReplacement =
175 | """
176 | |
177 | |
178 | | f replaced
179 | | item2
180 | |
181 | |
182 | | f replaced
183 | | item2
184 | |
185 | |
186 | | item1
187 | | item2
188 | | item3
189 | |
190 | | summary
191 | |
192 | """.stripMargin
193 |
194 | val xmlAsStringAfterTextReplacement =
195 | """
196 | |
197 | |
198 | |
199 | |
200 | |
201 | | item1
202 | | item2
203 | |
204 | |
205 | | f replaced
206 | | item2
207 | | item3
208 | | f replaced
209 | |
210 | |
211 | | f replaced
212 | | item2
213 | | item3
214 | | f replaced
215 | |
216 | |
217 | | summary
218 | |
219 | |
220 | |
221 | """.stripMargin
222 | }
223 |
--------------------------------------------------------------------------------