Skip to content

Commit

Permalink
Merge pull request #139 from geirolz/hotfix-regression-mutable-state
Browse files Browse the repository at this point in the history
Hotfix safe xml duplication
  • Loading branch information
geirolz authored Sep 28, 2023
2 parents 3bd3ce5 + dc1166c commit 6ed1743
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 140 deletions.
2 changes: 1 addition & 1 deletion core/example/modifying.sc
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ val data = xml"""
</wrapper>"""

val result: Modifier.Result[XmlNode] =
data.modify(_.root.foo.bar.baz.value.modifyIfNode(_.withText(2)))
data.modify(_.root.foo.bar.baz.value.modifyNode(_.withText(2)))
10 changes: 10 additions & 0 deletions core/src/main/scala/cats/xml/NodeContent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ sealed trait NodeContent {
case NodeContent.Children(children) => children.toList
case _ => Nil
}

final def duplicate: NodeContent =
this match {
case NodeContent.Empty =>
NodeContent.Empty
case NodeContent.Text(data) =>
NodeContent.Text(data)
case NodeContent.Children(children: NonEmptyList[XmlNode]) =>
NodeContent.Children(children.map(_.duplicate))
}
}
object NodeContent extends NodeContentInstances {

Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/xml/Xml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ trait Xml {
case _ => None
}

/** Create a new immutable instance with the same values of the current one
*
* @return
* A new instance with the same values of the current one
*/
def duplicate: Xml

override def equals(obj: Any): Boolean =
obj match {
case obj: Xml => Xml.eqXml.eqv(this, obj)
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/cats/xml/cursor/AttrCursor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class AttrCursor(protected val vCursor: NodeCursor, op: AttrCursor.Op)
$this.focus(node) match {
case Right(attr: XmlAttribute) =>
vCursor
.modifyIfNode(_.updateAttr(attr.key)(modifier))
.modifyNode(_.updateAttr(attr.key)(modifier))
.apply(node)
case Left(failure) =>
ModifierFailure.CursorFailed(NonEmptyList.one(failure)).asLeft
Expand All @@ -39,7 +39,7 @@ final class AttrCursor(protected val vCursor: NodeCursor, op: AttrCursor.Op)
attr.mapDecode(f) match {
case Valid(newAttrValue) =>
vCursor
.modifyIfNode(_.updateAttr(attr.key)(_ => newAttrValue))
.modifyNode(_.updateAttr(attr.key)(_ => newAttrValue))
.apply(node)
case Invalid(failures: NonEmptyList[DecoderFailure]) =>
ModifierFailure.DecoderFailed(failures).asLeft
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/cats/xml/cursor/NodeCursor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ sealed trait NodeCursor
}

// modify
def modifyIfNode(modifier: Endo[XmlNode.Node]): Modifier[XmlNode] =
def modifyNode(modifier: Endo[XmlNode.Node]): Modifier[XmlNode] =
modify(_.fold(ifNode = modifier, ifGroup = identity))

def modifyIfGroup(modifier: Endo[XmlNode.Group]): Modifier[XmlNode] =
def modifyGroup(modifier: Endo[XmlNode.Group]): Modifier[XmlNode] =
modify(_.fold(ifNode = identity, ifGroup = modifier))

override def modify(modifier: Endo[XmlNode]): Modifier[XmlNode] =
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/cats/xml/cursor/TextCursor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class TextCursor(protected[xml] val lastCursor: NodeCursor)
Modifier(node =>
$this.as[T].focus(node) match {
case Validated.Valid(textValue) =>
lastCursor.modifyIfNode(_.withText(f(textValue)))(node)
lastCursor.modifyNode(_.withText(f(textValue)))(node)
case Validated.Invalid(failures) =>
ModifierFailure.CursorFailed(failures).asLeft
}
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/xml/xmlAttribute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ final case class XmlAttribute(key: String, value: XmlData) extends Xml with Seri
dummyImplicit: DummyImplicit
): Boolean =
exists(_ == key, (data: XmlData) => valuep(data.asString))

def duplicate: XmlAttribute =
XmlAttribute(key, value.duplicate)
}
object XmlAttribute extends XmlAttributeSyntax with XmlAttributeInstances {

Expand Down
17 changes: 16 additions & 1 deletion core/src/main/scala/cats/xml/xmlData.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cats.xml

import cats.xml.XmlData.*
import cats.xml.codec.Decoder
import cats.{Eq, Order, Show}

Expand All @@ -23,9 +24,23 @@ sealed trait XmlData extends Xml with Serializable {
case XmlData.XmlArray(value) => value.isEmpty
case _ => false
}

override def duplicate: XmlData =
this match {
case XmlNull => XmlNull
case value: XmlString => value.copy()
case value: XmlChar => value.copy()
case value: XmlBool => value.copy()
case value: XmlArray[_] => value.copy()
case value: XmlLong => value.copy()
case value: XmlFloat => value.copy()
case value: XmlDouble => value.copy()
case value: XmlBigDecimal => value.copy()
}
}
case object XmlNull extends Xml with XmlNode.Null with XmlData {
override final def isEmpty: Boolean = true
override final def isEmpty: Boolean = true
override final def duplicate: XmlNull.type = this
// To avoid stackoverflow since the XmlPrinter used by the Eq instance uses patter matching.
override final def equals(obj: Any): Boolean = obj.isInstanceOf[XmlNull.type]
}
Expand Down
27 changes: 16 additions & 11 deletions core/src/main/scala/cats/xml/xmlNode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ sealed trait XmlNode extends Xml {
* @return
* A new instance with the same values of the current one
*/
def duplicate: Self
def duplicate: XmlNode

/** Convert the node to a group. If this instance already is a group it will be returned the same
* instance.
Expand Down Expand Up @@ -206,11 +206,11 @@ object XmlNode extends XmlNodeInstances with XmlNodeSyntax {

private[xml] trait Null extends XmlNode {
override type Self = Null
override def label: String = ""
override def attributes: List[XmlAttribute] = Nil
override def content: NodeContent = NodeContent.empty
override def duplicate: Self = this
override private[xml] def updateContent(f: Endo[NodeContent]): Self = this
override final def label: String = ""
override final def attributes: List[XmlAttribute] = Nil
override final def content: NodeContent = NodeContent.empty
override def duplicate: Self = this
override final private[xml] def updateContent(f: Endo[NodeContent]): Self = this
}

lazy val emptyGroup: XmlNode.Group = new Group(NodeContent.empty)
Expand Down Expand Up @@ -311,7 +311,11 @@ object XmlNode extends XmlNodeInstances with XmlNodeSyntax {

override def content: NodeContent = mContent

override def duplicate: XmlNode.Node = safeCopy()
override def duplicate: XmlNode.Node = safeCopy(
label = label,
attributes = attributes.map(_.duplicate),
content = content.duplicate
)

@impure
private[xml] def updateLabel(f: Endo[String]): XmlNode.Node =
Expand Down Expand Up @@ -377,7 +381,9 @@ object XmlNode extends XmlNodeInstances with XmlNodeSyntax {

override def content: NodeContent = mContent

override def duplicate: XmlNode.Group = safeCopy()
override def duplicate: XmlNode.Group = safeCopy(
content = content.duplicate
)

/** Convert current instance to a [[XmlNode.Node]].
*
Expand Down Expand Up @@ -416,9 +422,8 @@ object XmlNode extends XmlNodeInstances with XmlNodeSyntax {
@impure
private[xml] def safeCopy(
content: NodeContent = this.mContent
): XmlNode.Group = new Group(
unsafeRequireNotNull(content)
)
): XmlNode.Group =
new Group(unsafeRequireNotNull(content))

// -----------------------------//
/* ################################################ */
Expand Down
4 changes: 2 additions & 2 deletions core/src/test/scala/cats/xml/XmlPrinterSuite.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package cats.xml

import cats.xml.testing.{DataSize, Ops, XmlNodeGen}
import cats.xml.testing.{DataSize, Ops, XmlGen}
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

Expand Down Expand Up @@ -233,7 +233,7 @@ class XmlPrinterPerformanceSuite extends munit.ScalaCheckSuite {
property("XmlPrinter.default.prettyString with XL document") {

implicit val arbXmlNode: Arbitrary[XmlNode] = Arbitrary(
XmlNodeGen.genXmlNode(DataSize.L)
XmlGen.genXmlNode(DataSize.L)
)

forAll { (value: XmlNode) =>
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/cats/xml/codec/decoderSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class DecoderSuite extends munit.FunSuite {

class DecoderInstancesSuite extends munit.DisciplineSuite {

import cats.xml.testing.arbitrary.XmlDataArbitrary.*
import cats.xml.testing.arbitrary.XmlArbitrary.*

test("Decoder.decodeUnit") {
assertEquals(
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/cats/xml/codec/encoderSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class EncoderSuite extends munit.FunSuite {
class EncoderInstancesSuite extends munit.ScalaCheckSuite {

import cats.xml.testing.arbitrary.CommonArbitrary.*
import cats.xml.testing.arbitrary.XmlDataArbitrary.*
import cats.xml.testing.arbitrary.XmlArbitrary.*

property(s"Encoder.encodeOption") {
forAll { (value: Option[Int]) =>
Expand Down
41 changes: 41 additions & 0 deletions core/src/test/scala/cats/xml/modifier/ModifierSuite.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
package cats.xml.modifier

import cats.data.Validated.Valid
import cats.xml.XmlNode
import cats.xml.syntax.XmlAttrStringOps

class ModifierSuite extends munit.FunSuite {

test("Modifier works as expected") {
val node: XmlNode =
XmlNode("wrapper")
.withAttributes("a" := "1", "b" := "2")
.withChildren(
XmlNode("root")
.withAttributes("a" := "1", "b" := "2")
.withChildren(
XmlNode("foo")
.withChildren(
XmlNode("baz")
.withAttributes("a" := "1", "b" := "2")
.withChildren(
XmlNode("bar")
.withChildren(
XmlNode("value")
.withText(1)
)
)
)
)
)

val result: Modifier.Result[XmlNode] =
node.modify(_.root.foo.baz.bar.value.modifyNode(_.withText(2)))

assertEquals(
obtained = node.focus(_.root.foo.baz.bar.value.text.as[Int]),
expected = Valid(1)
)

assertEquals(
obtained = result.map(_.focus(_.root.foo.baz.bar.value.text.as[Int])),
expected = Right(Valid(2))
)
}

test("Modifier.id") {
assertEquals(
obtained = Modifier.id[String]("FOO"),
Expand Down
Loading

0 comments on commit 6ed1743

Please sign in to comment.