Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Seq and List decoding #138

Merged
merged 1 commit into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions core/src/main/scala/cats/xml/NodeContent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,30 @@

final val empty: NodeContent = Empty

def text[T: DataEncoder](data: T): Option[NodeContent] = {
def text[T: DataEncoder](data: T): NodeContent = {
val encData = DataEncoder[T].encode(data)
if (encData.isEmpty) None else Some(Text(encData))
if (encData.isEmpty) NodeContent.empty else Text(encData)
}

def textOrEmpty[T: DataEncoder](data: T): NodeContent =
text[T](data).getOrElse(NodeContent.empty)

def children(childrenLs: Seq[XmlNode]): Option[NodeContent] =
NonEmptyList.fromList(childrenLs.toList.filterNot(_.isNull)).map(Children(_))

def children(node: XmlNode, nodes: XmlNode*): NodeContent =
Children(NonEmptyList.of(node, nodes*))
children(node +: nodes)

Check warning on line 46 in core/src/main/scala/cats/xml/NodeContent.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/NodeContent.scala#L46

Added line #L46 was not covered by tests

def childrenNel(childrenNel: NonEmptyList[XmlNode]): NodeContent =
childrenOrEmpty(childrenNel.toList)

def childrenOrEmpty(childrenLs: Seq[XmlNode]): NodeContent =
children(childrenLs).getOrElse(NodeContent.empty)
children(childrenNel.toList)

def children(childrenLs: Seq[XmlNode]): NodeContent =
NonEmptyList

Check warning on line 52 in core/src/main/scala/cats/xml/NodeContent.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/NodeContent.scala#L52

Added line #L52 was not covered by tests
.fromList(
childrenLs
.flatMap(
_.fold(

Check warning on line 56 in core/src/main/scala/cats/xml/NodeContent.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/NodeContent.scala#L54-L56

Added lines #L54 - L56 were not covered by tests
ifNode = Seq(_),
ifGroup = _.children
).filterNot(_.isNull)
)

Check warning on line 60 in core/src/main/scala/cats/xml/NodeContent.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/NodeContent.scala#L60

Added line #L60 was not covered by tests
.toList
)
.fold(NodeContent.empty)(Children(_))

case object Empty extends NodeContent
final case class Text(data: XmlData) extends NodeContent
Expand Down
18 changes: 12 additions & 6 deletions core/src/main/scala/cats/xml/codec/Decoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,21 @@
implicit def decoderLiftToSeq[F[X] <: Seq[X], T: Decoder](implicit
f: Factory[T, F[T]]
): Decoder[F[T]] =
decodeString
.flatMapF(str => {
str
.split(",")
.map(s => Decoder[T].decode(Xml.string(s)))
Decoder.instance {
case XmlArray(value) =>
value
.map(Decoder[T].decode)

Check warning on line 268 in core/src/main/scala/cats/xml/codec/Decoder.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/codec/Decoder.scala#L266-L268

Added lines #L266 - L268 were not covered by tests
.toVector
.sequence
.map(_.to(f))
})
case group: XmlNode.Group =>
group.children

Check warning on line 273 in core/src/main/scala/cats/xml/codec/Decoder.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/codec/Decoder.scala#L272-L273

Added lines #L272 - L273 were not covered by tests
.map(Decoder[T].decode)
.toVector
.sequence
.map(_.to(f))
case other => Decoder[T].decode(other).map(Seq(_).to(f))

Check warning on line 278 in core/src/main/scala/cats/xml/codec/Decoder.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/cats/xml/codec/Decoder.scala#L278

Added line #L278 was not covered by tests
}

implicit def decodeCatsNel[T: Decoder]: Decoder[NonEmptyList[T]] =
decoderLiftToSeq[Vector, T].flatMapF {
Expand Down
8 changes: 4 additions & 4 deletions core/src/main/scala/cats/xml/xmlNode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@
elements.toList match {
case Nil => XmlNode.emptyGroup
case ::(head, Nil) => head
case all => new Group(NodeContent.childrenOrEmpty(all))
case all => new Group(NodeContent.children(all))

Check notice

Code scanning / Scalastyle (reported by Codacy)

Lowercase pattern match (surround with ``, or add : Any). Note

Lowercase pattern match (surround with ``, or add : Any).

Check notice

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibit case statement pattern match from being lowercase. Note

Lower case pattern matching.
}

/** Create a new [[XmlNode.Group]] instance with the specified [[XmlNode]]s
Expand All @@ -284,7 +284,7 @@
* A new [[XmlNode.Group]] instance with the specified [[XmlNode]]s
*/
def group(elements: Seq[XmlNode]): XmlNode.Group =
new Group(NodeContent.childrenOrEmpty(elements))
new Group(NodeContent.children(elements))

// --------------------- XML NODE ---------------------
/** Represent a simple single XML node.
Expand Down Expand Up @@ -543,7 +543,7 @@
withChildren(child +: children)

def withChildren(children: Seq[XmlNode]): Self =
withContent(NodeContent.children(children).getOrElse(NodeContent.empty))
withContent(NodeContent.children(children))

def appendChildren(child: XmlNode, children: XmlNode*): Self =
updateChildren(currentChildren => currentChildren ++ List(child) ++ children)
Expand Down Expand Up @@ -614,7 +614,7 @@
/** Set node content to Text with the specified data. All children nodes will be removed.
*/
def withText[T: DataEncoder](data: T): XmlNode.Node =
node.withContent(NodeContent.textOrEmpty(data))
node.withContent(NodeContent.text(data))

/** Decode and then update node text if content is text.
*
Expand Down
6 changes: 3 additions & 3 deletions core/src/test/scala/cats/xml/NodeContentSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ class NodeContentSuite extends munit.ScalaCheckSuite {

test("NodeContent.text('FOO') is NOT empty") {
assert(
NodeContent.textOrEmpty("FOO").nonEmpty
NodeContent.text("FOO").nonEmpty
)
}

test("NodeContent.text('') is empty") {
assert(
NodeContent.textOrEmpty("").isEmpty
NodeContent.text("").isEmpty
)
}

Expand All @@ -48,7 +48,7 @@ class NodeContentSuite extends munit.ScalaCheckSuite {
property(s"NodeContent.text create content with ${c.runtimeClass.getSimpleName}") {
forAll { (value: T) =>
assertEquals(
obtained = NodeContent.textOrEmpty(value).text.flatMap(_.as[T].toOption),
obtained = NodeContent.text(value).text.flatMap(_.as[T].toOption),
expected = if (DataEncoder[T].encode(value).isEmpty) None else Some(value)
)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/cats/xml/XmlNodeSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

test("XmlNode.apply") {
assertEquals(
obtained = XmlNode("Foo", List("A" := 1), NodeContent.textOrEmpty("Text")),
obtained = XmlNode("Foo", List("A" := 1), NodeContent.text("Text")),

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "A" appears 35 times in the file. Note test

The string literal "A" appears 35 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'A' appears 35 times in the file.
expected = XmlNode("Foo").withAttributes("A" := 1).withText("Text")
)
intercept[IllegalArgumentException](
Expand Down
114 changes: 114 additions & 0 deletions core/src/test/scala/cats/xml/codec/DecoderSeqSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cats.xml.codec

Check notice

Code scanning / Scalastyle (reported by Codacy)

File must not end with newline character. Note test

File must not end with newline character.

Check notice

Code scanning / Scalastyle (reported by Codacy)

Header does not match expected text. Note test

Header does not match expected text.

import cats.data.Validated.Valid
import cats.implicits.{catsSyntaxOptionId, catsSyntaxTuple2Semigroupal}

Check notice

Code scanning / Scalastyle (reported by Codacy)

Avoid block imports. Note test

Avoid block imports.
import cats.xml.XmlNode
import cats.xml.syntax.DecoderOps

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

This makes it easy to examine visually, and is simple to automate. Warning test

Imports should be sorted alphabetically

// TODO: Move to decoder suite

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

FIXME tags are commonly used to mark places where a bug is suspected Warning test

Take the required action to fix the issue indicated by this comment.
class DecoderSeqSuite extends munit.FunSuite {

case class Baz(v: String)
object Baz {
implicit val bazD: Decoder[Baz] =

Check notice

Code scanning / Scalastyle (reported by Codacy)

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'. Note test

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'.
Decoder.fromCursor(_.text.as[String]).map(Baz(_))
}

case class Qux(v: String)
object Qux {
implicit val quxD: Decoder[Qux] =

Check notice

Code scanning / Scalastyle (reported by Codacy)

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'. Note test

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'.
Decoder.fromCursor(_.text.as[String]).map(Qux(_))

}

case class Bar(baz: Baz, qux: Option[Qux])
object Bar {
implicit val barD: Decoder[Bar] =

Check notice

Code scanning / Scalastyle (reported by Codacy)

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'. Note test

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'.
Decoder.fromCursor { c =>
(
c.down("baz").as[Baz],

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "baz" appears 7 times in the file. Note test

The string literal "baz" appears 7 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'baz' appears 7 times in the file.
c.down("qux").as[Option[Qux]]

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "qux" appears 3 times in the file. Note test

The string literal "qux" appears 3 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'qux' appears 3 times in the file.
).mapN(Bar.apply)
}
}

case class Foo(bar: List[Bar])
object Foo {
implicit val fooD: Decoder[Foo] =

Check notice

Code scanning / Scalastyle (reported by Codacy)

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'. Note test

Field name does not match the regular expression '^[A-Z][A-Za-z0-9]*$'.
Decoder.fromCursor { c =>
c.down("bar")

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "bar" appears 7 times in the file. Note test

The string literal "bar" appears 7 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'bar' appears 7 times in the file.
.as[List[Bar]]
.map(Foo.apply)
}
}

test("Decoder[Seq[A]] with XmlNode") {

val data: XmlNode =
XmlNode("foo")

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "foo" appears 2 times in the file. Note test

The string literal "foo" appears 2 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'foo' appears 2 times in the file.
.withChildren(
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("1")

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "1" appears 4 times in the file. Note test

The string literal "1" appears 4 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal '1' appears 4 times in the file.
),
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("2")

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "2" appears 4 times in the file. Note test

The string literal "2" appears 4 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal '2' appears 4 times in the file.
),
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("3"),

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "3" appears 4 times in the file. Note test

The string literal "3" appears 4 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal '3' appears 4 times in the file.
XmlNode("qux").withText("A")

Check notice

Code scanning / Scalastyle (reported by Codacy)

The string literal "A" appears 4 times in the file. Note test

The string literal "A" appears 4 times in the file.

Check warning

Code scanning / Codacy-scalameta-pro (reported by Codacy)

Prohibits usage of duplicated string literals. Warning test

The string literal 'A' appears 4 times in the file.
)
)

assertEquals(
data.as[Foo],
Valid(
Foo(
List[Bar](
Bar(Baz("1"), None),
Bar(Baz("2"), None),
Bar(Baz("3"), Qux("A").some)
)
)
)
)
}

test("Decoder[Seq[A]] with XmlGroup") {

val data: XmlNode =
XmlNode("foo")
.withChildren(
XmlNode.group(
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("1")
),
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("2")
),
XmlNode("bar")
.withChildren(
XmlNode("baz").withText("3"),
XmlNode("qux").withText("A")
)
)
)

assertEquals(
data.as[Foo],
Valid(
Foo(
List[Bar](
Bar(Baz("1"), None),
Bar(Baz("2"), None),
Bar(Baz("3"), Qux("A").some)
)
)
)
)
}
}
27 changes: 23 additions & 4 deletions core/src/test/scala/cats/xml/cursor/NodeCursorSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,34 @@ class NodeCursorSuite extends munit.FunSuite {
)
}

test("NodeCursor.down with group") {

val node: XmlNode = {
test("NodeCursor.down returns a group") {

val node: XmlNode =
XmlNode("Root").withChildren(
XmlNode("Foo").withChildren(XmlNode("Value").withText(1)),
XmlNode("Foo").withChildren(XmlNode("Value").withText(2))
)
}

assertEquals(
obtained = Root.down("Foo").down("Value").focus(node),
expected = Right(
XmlNode.group(
XmlNode("Value").withText(1),
XmlNode("Value").withText(2)
)
)
)
}

test("NodeCursor.down with child group") {

val node: XmlNode =
XmlNode("Root").withChildren(
XmlNode.group(
XmlNode("Foo").withChildren(XmlNode("Value").withText(1)),
XmlNode("Foo").withChildren(XmlNode("Value").withText(2))
)
)

assertEquals(
obtained = Root.down("Foo").down("Value").focus(node),
Expand Down
8 changes: 4 additions & 4 deletions core/src/test/scala/cats/xml/testing/XmlNodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ object XmlNodeGen {
maxTextSize: Int = 100
): Gen[XmlNode] = {

def genChildren: Gen[Option[NodeContent]] =
def genChildren: Gen[NodeContent] =
if (maxDeep > 0)
Gen.lzy(
Gen
Expand All @@ -83,16 +83,16 @@ object XmlNodeGen {
.map(NodeContent.children(_))
)
else
Gen.const(None)
Gen.const(NodeContent.empty)

for {
nodeName <- Gen.lzy(getNonEmptyString(maxNodeName))
attributes <- Gen.lzy(genXmlAttributes(maxAttrs, maxAttrNameSize, maxAttrValueSize))
content <- Gen.lzy(
Gen.frequency(
2 -> Gen.const(NodeContent.empty),
18 -> Gen.lzy(getNonEmptyString(maxTextSize).map(NodeContent.textOrEmpty(_))),
80 -> Gen.lzy(genChildren.map(_.getOrElse(NodeContent.empty)))
18 -> Gen.lzy(getNonEmptyString(maxTextSize).map(NodeContent.text(_))),
80 -> Gen.lzy(genChildren)
)
)
} yield XmlNode(nodeName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
XmlNode(
label = e.label,
attributes = XmlAttribute.fromMetaData(e.attributes),
content = NodeContent.textOrEmpty(e.text.trim)
content = NodeContent.text(e.text.trim)

Check warning on line 21 in modules/standard/src/main/scala/cats/xml/std/nodeSeqConverter.scala

View check run for this annotation

Codecov / codecov/patch

modules/standard/src/main/scala/cats/xml/std/nodeSeqConverter.scala#L21

Added line #L21 was not covered by tests
)
case e: Elem =>
val tree = XmlNode(
Expand All @@ -36,15 +36,15 @@
val content = if (neChildLen > 0) {
val head = neChild.head
if (head.isAtom) {
NodeContent.textOrEmpty(head.asInstanceOf[Atom[?]].data.toString.trim)
NodeContent.text(head.asInstanceOf[Atom[?]].data.toString.trim)
} else {

val res: Array[XmlNode] = new Array[XmlNode](neChildLen)
for (idx <- 0 until neChildLen) {
res.update(idx, fromNodeSeq(neChild(idx)))
}

NodeContent.children(res.toList).getOrElse(NodeContent.empty)
NodeContent.children(res.toList)
}

} else NodeContent.empty
Expand Down
Loading