diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 4352941..31ff6f8 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -30,7 +30,7 @@ jobs:
include:
- scala: 2.13.8
name: Scala2
- test-tasks: coverage test coverageReport
+ test-tasks: coverage test coverageReport generate-docs
- scala: 3.1.3
name: Scala3
test-tasks: test # scoverage doesn’t support Scala 3
@@ -39,10 +39,11 @@ jobs:
- uses: actions/checkout@v3
#----------- JDK -----------
- - name: Set up JDK 11
- uses: actions/setup-java@v1
+ - name: Setup JDK
+ uses: actions/setup-java@v3
with:
- java-version: 11
+ distribution: "liberica"
+ java-version: 17
#----------- CACHE -----------
- name: Cache SBT
@@ -71,10 +72,11 @@ jobs:
- uses: actions/checkout@v3
#----------- JDK -----------
- - name: Set up JDK 11
- uses: actions/setup-java@v1
+ - name: Setup JDK
+ uses: actions/setup-java@v3
with:
- java-version: 11
+ distribution: "liberica"
+ java-version: 17
#----------- CACHE -----------
- name: Cache SBT
diff --git a/.java-version b/.java-version
new file mode 100644
index 0000000..03b6389
--- /dev/null
+++ b/.java-version
@@ -0,0 +1 @@
+17.0
diff --git a/README.md b/README.md
index 9d230f7..7058f43 100644
--- a/README.md
+++ b/README.md
@@ -17,15 +17,22 @@ libraryDependencies += "com.github.geirolz" %% "cats-xml" % "0.0.3"
This library is not production ready yet. There is a lot of work to do to complete it:
- ~~There are some performance issues loading xml from strings or files due the fact that there is a double
conversion, We need to parse bytes directly into `cats-xml` classes.~~
-- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
+~~- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
can be done in some way thinking about attributes with primitives and value classes BUT distinguish between a Node/Attribute and Text
- is hard, probably an annotation or a custom Decoder/Encoder is required.
+ is hard, probably an annotation or a custom Decoder/Encoder is required.~~
- Reach a good code coverage with the tests (using munit)
- Improve documentation
+- Literal macros to check XML strings at compile time
Contributions are more than welcome 💪
-Given `Foo` class
+## Modules
+- [Effect](docs/compiled/effect.md)
+- [Generic](docs/compiled/generic.md)
+- [Standard](docs/compiled/standard.md)
+
+## Example
+Given
```scala
case class Foo(
foo: Option[String],
@@ -61,7 +68,7 @@ val encoder: Encoder[Foo] = Encoder.of(t =>
XmlNode("Foo")
.withAttributes(
"foo" := t.foo.getOrElse("ERROR"),
- "bar" := t.bar,
+ "bar" := t.bar
)
.withText(t.text)
)
@@ -86,4 +93,4 @@ val result: Modifier.Result[XmlNode] = Root
// result: Modifier.Result[XmlNode] = Right(
// value = NEW
// )
-```
+```
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index e273d8d..bb8af52 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,11 +1,14 @@
import sbt.project
-val prjName = "cats-xml"
-val org = "com.github.geirolz"
+lazy val scala213 = "2.13.8"
+lazy val scala31 = "3.1.3"
+lazy val supportedScalaVersions = List(scala213, scala31)
+lazy val prjName = "cats-xml"
+lazy val org = "com.github.geirolz"
//## global project to no publish ##
val copyReadMe = taskKey[Unit]("Copy generated README to main folder.")
-lazy val catsxml: Project = project
+lazy val `cats-xml`: Project = project
.in(file("."))
.settings(
inThisBuild(
@@ -27,15 +30,13 @@ lazy val catsxml: Project = project
.settings(baseSettings)
.settings(noPublishSettings)
.settings(
- name := prjName,
- description := "A purely functional XML library",
- organization := org,
-
- // docs
- copyReadMe := IO.copyFile(file("docs/compiled/README.md"), file("README.md")),
- (Compile / compile) := (Compile / compile)
- .dependsOn(copyReadMe.toTask.dependsOn((docs / mdoc).toTask("")))
- .value
+ name := prjName,
+ description := "A purely functional XML library",
+ organization := org,
+ crossScalaVersions := Nil
+ )
+ .settings(
+ copyReadMe := IO.copyFile(file("docs/compiled/README.md"), file("README.md"))
)
.aggregate(docs, core, metrics, utils, effect, scalaxml)
@@ -43,7 +44,7 @@ lazy val docs: Project =
project
.in(file("docs"))
.enablePlugins(MdocPlugin)
- .dependsOn(core)
+ .dependsOn(core, effect, generic, scalaxml)
.settings(
baseSettings,
noPublishSettings,
@@ -60,39 +61,39 @@ lazy val docs: Project =
)
)
+lazy val utils: Project =
+ buildModule(
+ prjModuleName = "utils",
+ toPublish = false,
+ folder = "."
+ ).settings(
+ libraryDependencies ++= ProjectDependencies.Utils.dedicated
+ )
+
lazy val core: Project =
buildModule(
prjModuleName = "core",
toPublish = true,
folder = "."
- )
+ ).dependsOn(utils)
lazy val metrics: Project =
buildModule(
prjModuleName = "metrics",
toPublish = false,
folder = "."
- ).dependsOn(core, effect, scalaxml, generic)
+ ).dependsOn(core, utils, effect, scalaxml, generic)
.settings(
libraryDependencies ++= ProjectDependencies.Metrics.dedicated
)
-lazy val utils: Project =
- buildModule(
- prjModuleName = "utils",
- toPublish = false,
- folder = "."
- ).settings(
- libraryDependencies ++= ProjectDependencies.Utils.dedicated
- )
-
// modules
lazy val effect: Project =
buildModule(
prjModuleName = "effect",
toPublish = true,
folder = "modules"
- ).dependsOn(core)
+ ).dependsOn(core, utils)
.settings(
libraryDependencies ++= ProjectDependencies.Effect.dedicated
)
@@ -102,7 +103,7 @@ lazy val scalaxml: Project =
prjModuleName = "standard",
toPublish = true,
folder = "modules"
- ).dependsOn(core)
+ ).dependsOn(core, utils)
.settings(
libraryDependencies ++= ProjectDependencies.Standard.dedicated
)
@@ -152,8 +153,8 @@ lazy val noPublishSettings: Seq[Def.Setting[_]] = Seq(
lazy val baseSettings: Seq[Def.Setting[_]] = Seq(
// scala
- crossScalaVersions := List("2.13.8", "3.1.3"),
- scalaVersion := crossScalaVersions.value.head,
+ crossScalaVersions := supportedScalaVersions,
+ scalaVersion := supportedScalaVersions.head,
scalacOptions ++= scalacSettings(scalaVersion.value),
// dependencies
resolvers ++= ProjectResolvers.all,
@@ -231,3 +232,4 @@ def scalacSettings(scalaVersion: String): Seq[String] =
//=============================== ALIASES ===============================
addCommandAlias("check", "scalafmtAll;clean;coverage;test;coverageAggregate")
+addCommandAlias("generate-docs", "mdoc;copyReadMe;")
diff --git a/core/src/main/scala/cats/xml/Xml.scala b/core/src/main/scala/cats/xml/Xml.scala
index 6049428..36ed6eb 100644
--- a/core/src/main/scala/cats/xml/Xml.scala
+++ b/core/src/main/scala/cats/xml/Xml.scala
@@ -1,6 +1,6 @@
package cats.xml
-import cats.{MonadThrow, Show}
+import cats.{xml, Show}
import cats.xml.codec.Decoder
import cats.xml.Xml.XmlNull
@@ -42,11 +42,7 @@ object Xml {
case object XmlNull extends Xml with XmlData
final lazy val Null: Xml & XmlData = XmlNull
-
- def fromString[F[_]: MonadThrow](xmlString: String)(implicit
- parser: XmlParser[F]
- ): F[XmlNode] =
- parser.parseString(xmlString)
+ final lazy val Data: XmlData.type = xml.XmlData
}
sealed trait XmlData extends Xml with Serializable {
@@ -65,12 +61,6 @@ sealed trait XmlData extends Xml with Serializable {
}
object XmlData {
- case class XmlString(value: String) extends XmlData
- case class XmlNumber[T <: Number](value: T) extends XmlData
- case class XmlArray[T <: XmlData](value: Array[T]) extends XmlData
- case class XmlByte(value: Byte) extends XmlData
- case class XmlBool(value: Boolean) extends XmlData
-
lazy val True: XmlData = fromBoolean(true)
lazy val False: XmlData = fromBoolean(false)
lazy val empty: XmlData = fromString("")
@@ -83,6 +73,12 @@ object XmlData {
def fromByte(value: Byte): XmlData = XmlByte(value)
def fromBoolean(value: Boolean): XmlData = XmlBool(value)
+ private[xml] case class XmlString(value: String) extends XmlData
+ private[xml] case class XmlNumber[T <: Number](value: T) extends XmlData
+ private[xml] case class XmlArray[T <: XmlData](value: Array[T]) extends XmlData
+ private[xml] case class XmlByte(value: Byte) extends XmlData
+ private[xml] case class XmlBool(value: Boolean) extends XmlData
+
// TODO: TO CHECK EQ TO DECODE STRING
implicit val showXmlData: Show[XmlData] = {
case XmlNull => ""
diff --git a/core/src/main/scala/cats/xml/XmlPrinter.scala b/core/src/main/scala/cats/xml/XmlPrinter.scala
index 8d23336..ee97664 100644
--- a/core/src/main/scala/cats/xml/XmlPrinter.scala
+++ b/core/src/main/scala/cats/xml/XmlPrinter.scala
@@ -2,6 +2,7 @@ package cats.xml
import cats.xml.Xml.XmlNull
import cats.xml.XmlNode.XmlNodeGroup
+import cats.xml.utils.format.Indentator
import scala.annotation.{tailrec, unused}
import scala.collection.mutable
@@ -22,41 +23,6 @@ object XmlPrinter {
implicit val default: Config = Config()
}
- case class Indentator(char: Char, size: Int, depth: Int, indentation: String) {
-
- private val unit = Indentator.buildString(char, size, 1)
-
- def forward: Indentator = this.copy(
- depth = depth + 1,
- indentation = indentation + unit
- )
-
- def backward: Indentator = this.copy(
- depth = depth - 1,
- indentation = if (indentation.nonEmpty) indentation.drop(1) else indentation
- )
- }
- object Indentator {
-
- def root(char: Char, size: Int): Indentator =
- build(
- char = char,
- size = size,
- depth = 0
- )
-
- def build(char: Char, size: Int, depth: Int): Indentator =
- Indentator(
- char = char,
- size = size,
- depth = depth,
- indentation = (0 until size * depth).map(_ => char).mkString
- )
-
- def buildString(char: Char, size: Int, depth: Int): String =
- (0 until size * depth).map(_ => char).mkString
- }
-
// --------------------------------------------------------------//
def stringify(xml: Xml): String =
prettyString(xml = xml)(
diff --git a/core/src/main/scala/cats/xml/codec/Decoder.scala b/core/src/main/scala/cats/xml/codec/Decoder.scala
index b6316d5..8019f9c 100644
--- a/core/src/main/scala/cats/xml/codec/Decoder.scala
+++ b/core/src/main/scala/cats/xml/codec/Decoder.scala
@@ -166,8 +166,8 @@ sealed private[xml] trait DecoderDataInstances {
implicit val decodeLong: Decoder[Long] = decodeString.emapTry(s => Try(s.toLong))
implicit val decodeFloat: Decoder[Float] = decodeString.emapTry(s => Try(s.toFloat))
implicit val decodeDouble: Decoder[Double] = decodeString.emapTry(s => Try(s.toDouble))
- implicit val decodeBigDecimal: Decoder[BigDecimal] =
- decodeString.emapTry(s => Try(BigDecimal(s)))
+ implicit val decodeBigDecimal: Decoder[BigDecimal] = decodeString.emapTry(s => Try(BigDecimal(s)))
+ implicit val decodeBigInt: Decoder[BigInt] = decodeString.emapTry(s => Try(BigInt(s)))
}
sealed private[xml] trait DecoderLifterInstances { this: DecoderDataInstances =>
@@ -197,7 +197,7 @@ sealed private[xml] trait DecoderLifterInstances { this: DecoderDataInstances =>
.flatMapF(str => {
str
.split(",")
- .map(s => Decoder[T].decode(XmlString(s)))
+ .map(s => Decoder[T].decode(Xml.Data.fromString(s)))
.toVector
.sequence
.map(_.to(f))
diff --git a/core/src/main/scala/cats/xml/codec/DecoderFailure.scala b/core/src/main/scala/cats/xml/codec/DecoderFailure.scala
index b18d2fe..60850a3 100644
--- a/core/src/main/scala/cats/xml/codec/DecoderFailure.scala
+++ b/core/src/main/scala/cats/xml/codec/DecoderFailure.scala
@@ -5,7 +5,7 @@ import cats.data.NonEmptyList
import cats.xml.Xml
import cats.xml.codec.DecoderFailure.DecoderFailureException
import cats.xml.cursor.CursorFailure
-import cats.xml.utils.ErrorKeeper
+import cats.xml.utils.UnderlyingThrowable
sealed trait DecoderFailure {
@@ -17,7 +17,7 @@ object DecoderFailure extends DecoderFailureSyntax {
case class NoTextAvailable(subject: Xml) extends DecoderFailure
case class CursorFailed(failure: CursorFailure) extends DecoderFailure
case class CoproductNoMatch[+T](actual: Any, coproductValues: Seq[T]) extends DecoderFailure
- case class Error(error: Throwable) extends DecoderFailure with ErrorKeeper
+ case class Error(error: Throwable) extends DecoderFailure with UnderlyingThrowable
case class Custom(message: String) extends DecoderFailure
case class DecoderFailureException(failures: NonEmptyList[DecoderFailure])
diff --git a/core/src/main/scala/cats/xml/codec/Encoder.scala b/core/src/main/scala/cats/xml/codec/Encoder.scala
index 169a7eb..834aea3 100644
--- a/core/src/main/scala/cats/xml/codec/Encoder.scala
+++ b/core/src/main/scala/cats/xml/codec/Encoder.scala
@@ -2,7 +2,6 @@ package cats.xml.codec
import cats.xml.{Xml, XmlData}
import cats.Contravariant
-import cats.xml.XmlData.XmlString
// T => XML
trait Encoder[-T] {
@@ -55,7 +54,7 @@ private[xml] trait EncoderPrimitivesInstances {
implicit val encoderXmlData: DataEncoder[XmlData] = DataEncoder.of(identity)
implicit val encoderUnit: DataEncoder[Unit] = DataEncoder.of(_ => Xml.Null)
- implicit val encoderString: DataEncoder[String] = DataEncoder.of(XmlString(_))
+ implicit val encoderString: DataEncoder[String] = DataEncoder.of(Xml.Data.fromString(_))
implicit val encoderBoolean: DataEncoder[Boolean] = encoderString.contramap {
case true => "true"
case false => "false"
@@ -66,6 +65,7 @@ private[xml] trait EncoderPrimitivesInstances {
implicit val encoderFloat: DataEncoder[Float] = encoderString.contramap(_.toString)
implicit val encoderDouble: DataEncoder[Double] = encoderString.contramap(_.toString)
implicit val encoderBigDecimal: DataEncoder[BigDecimal] = encoderString.contramap(_.toString)
+ implicit val encoderBigInt: DataEncoder[BigInt] = encoderString.contramap(_.toString)
}
// #################### DATA ENCODER ####################
diff --git a/core/src/main/scala/cats/xml/cursor/CursorFailure.scala b/core/src/main/scala/cats/xml/cursor/CursorFailure.scala
index 86cb169..a08d924 100644
--- a/core/src/main/scala/cats/xml/cursor/CursorFailure.scala
+++ b/core/src/main/scala/cats/xml/cursor/CursorFailure.scala
@@ -4,7 +4,7 @@ import cats.{Eq, Show}
import cats.data.NonEmptyList
import cats.xml.codec.DecoderFailure
import cats.xml.cursor.CursorFailure.CursorFailureException
-import cats.xml.utils.ErrorKeeper
+import cats.xml.utils.UnderlyingThrowable
import cats.xml.XmlNode
/** A coproduct ADT to represent the `Cursor` possible failures.
@@ -52,7 +52,7 @@ object CursorFailure {
case class LeftBoundLimitAttr(path: String, lastKey: String) extends FailedAttribute with Missing
case class RightBoundLimitAttr(path: String, lastKey: String) extends FailedAttribute with Missing
case class Custom(message: String) extends CursorFailure
- case class Error(error: Throwable) extends CursorFailure with ErrorKeeper
+ case class Error(error: Throwable) extends CursorFailure with UnderlyingThrowable
case class CursorFailureException(failure: CursorFailure)
extends RuntimeException(s"Cursor failure: $failure")
diff --git a/core/src/main/scala/cats/xml/JavaConverters.scala b/core/src/main/scala/cats/xml/javaConverters.scala
similarity index 97%
rename from core/src/main/scala/cats/xml/JavaConverters.scala
rename to core/src/main/scala/cats/xml/javaConverters.scala
index 2a08ff0..c764188 100644
--- a/core/src/main/scala/cats/xml/JavaConverters.scala
+++ b/core/src/main/scala/cats/xml/javaConverters.scala
@@ -49,7 +49,7 @@ object JavaConverters extends JavaConvertersSyntax {
}
}
-trait JavaConvertersSyntax {
+private[xml] sealed trait JavaConvertersSyntax {
implicit class JDocumentOps(doc: JDocument) {
def asCatsXml: XmlNode = JavaConverters.nodeFromJavaDocument(doc)
diff --git a/core/src/main/scala/cats/xml/modifier/ModifierFailure.scala b/core/src/main/scala/cats/xml/modifier/ModifierFailure.scala
index 206ad12..018a1b5 100644
--- a/core/src/main/scala/cats/xml/modifier/ModifierFailure.scala
+++ b/core/src/main/scala/cats/xml/modifier/ModifierFailure.scala
@@ -3,7 +3,7 @@ package cats.xml.modifier
import cats.Show
import cats.xml.cursor.CursorFailure
import cats.xml.modifier.ModifierFailure.ModifierFailureException
-import cats.xml.utils.ErrorKeeper
+import cats.xml.utils.UnderlyingThrowable
/** A coproduct ADT to represent the `Modifier` possible failures.
*/
@@ -18,7 +18,7 @@ object ModifierFailure {
case class InvalidData[D](message: String, data: D) extends ModifierFailure
case class CursorFailed(cursorFailure: CursorFailure) extends ModifierFailure
case class Custom(message: String) extends ModifierFailure
- case class Error(error: Throwable) extends ModifierFailure with ErrorKeeper
+ case class Error(error: Throwable) extends ModifierFailure with UnderlyingThrowable
case class ModifierFailureException(failure: ModifierFailure)
extends RuntimeException(s"Modifier failure: $failure")
diff --git a/core/src/test/scala/cats/xml/NodeContentSuite.scala b/core/src/test/scala/cats/xml/NodeContentSuite.scala
index 02ab959..9c8f64e 100644
--- a/core/src/test/scala/cats/xml/NodeContentSuite.scala
+++ b/core/src/test/scala/cats/xml/NodeContentSuite.scala
@@ -40,6 +40,7 @@ class NodeContentSuite extends munit.ScalaCheckSuite {
testContentTextIso[Boolean]
testContentTextIso[String]
testContentTextIso[BigDecimal]
+ testContentTextIso[BigInt]
private def testContentTextIso[T: Arbitrary: DataEncoder: Decoder](implicit
c: ClassTag[T]
diff --git a/core/src/test/scala/cats/xml/XmlAttributeSuite.scala b/core/src/test/scala/cats/xml/XmlAttributeSuite.scala
index e16889a..bd12d23 100644
--- a/core/src/test/scala/cats/xml/XmlAttributeSuite.scala
+++ b/core/src/test/scala/cats/xml/XmlAttributeSuite.scala
@@ -16,6 +16,7 @@ class XmlAttributeSuite extends munit.ScalaCheckSuite {
testAttributeDataIso[Boolean]
testAttributeDataIso[String]
testAttributeDataIso[BigDecimal]
+ testAttributeDataIso[BigInt]
// equality
testAttributeEquality[Unit]
@@ -25,6 +26,7 @@ class XmlAttributeSuite extends munit.ScalaCheckSuite {
testAttributeEquality[Boolean]
testAttributeEquality[String]
testAttributeEquality[BigDecimal]
+ testAttributeEquality[BigInt]
private def testAttributeDataIso[T: Arbitrary: DataEncoder: Decoder](implicit
c: ClassTag[T]
diff --git a/core/src/test/scala/cats/xml/codec/decoderSuite.scala b/core/src/test/scala/cats/xml/codec/decoderSuite.scala
index 3c64a7f..1498a09 100644
--- a/core/src/test/scala/cats/xml/codec/decoderSuite.scala
+++ b/core/src/test/scala/cats/xml/codec/decoderSuite.scala
@@ -239,7 +239,7 @@ class DecoderLifterSuite extends munit.ScalaCheckSuite {
property(s"Decoder[${tag.runtimeClass.getSimpleName}] with Cursor success") {
forAll { (value: T) =>
assertEquals(
- obtained = Decoder[F[T]].decodeCursorResult(Right(XmlString(value.toString))),
+ obtained = Decoder[F[T]].decodeCursorResult(Right(Xml.Data.fromString(value.toString))),
expected = Valid(F.pure(value))
)
}
diff --git a/core/src/test/scala/cats/xml/cursor/NodeCursorSuite.scala b/core/src/test/scala/cats/xml/cursor/NodeCursorSuite.scala
index 81ba06d..d5fd3d6 100644
--- a/core/src/test/scala/cats/xml/cursor/NodeCursorSuite.scala
+++ b/core/src/test/scala/cats/xml/cursor/NodeCursorSuite.scala
@@ -1,8 +1,7 @@
package cats.xml.cursor
-import cats.xml.{XmlAttribute, XmlNode}
+import cats.xml.{Xml, XmlAttribute, XmlNode}
import cats.xml.cursor.NodeCursor.Root
-import cats.xml.XmlData.XmlString
class NodeCursorSuite extends munit.FunSuite {
@@ -376,7 +375,7 @@ class NodeCursorSuite extends munit.FunSuite {
assertEquals(
obtained = Root.foo.text.focus(node),
- expected = Right(XmlString("TEST"))
+ expected = Right(Xml.Data.fromString("TEST"))
)
assertEquals(
diff --git a/docs/compiled/README.md b/docs/compiled/README.md
index 889cc17..7058f43 100644
--- a/docs/compiled/README.md
+++ b/docs/compiled/README.md
@@ -11,21 +11,28 @@
A functional library to work with XML in Scala using cats core.
```sbt
-libraryDependencies += "com.github.geirolz" %% "cats-xml" % "0.0.2"
+libraryDependencies += "com.github.geirolz" %% "cats-xml" % "0.0.3"
```
This library is not production ready yet. There is a lot of work to do to complete it:
- ~~There are some performance issues loading xml from strings or files due the fact that there is a double
conversion, We need to parse bytes directly into `cats-xml` classes.~~
-- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
+~~- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
can be done in some way thinking about attributes with primitives and value classes BUT distinguish between a Node/Attribute and Text
- is hard, probably an annotation or a custom Decoder/Encoder is required.
+ is hard, probably an annotation or a custom Decoder/Encoder is required.~~
- Reach a good code coverage with the tests (using munit)
- Improve documentation
+- Literal macros to check XML strings at compile time
Contributions are more than welcome 💪
-Given `Foo` class
+## Modules
+- [Effect](docs/compiled/effect.md)
+- [Generic](docs/compiled/generic.md)
+- [Standard](docs/compiled/standard.md)
+
+## Example
+Given
```scala
case class Foo(
foo: Option[String],
@@ -53,7 +60,6 @@ val decoder: Decoder[Foo] =
### Encoder
-
```scala
import cats.xml.XmlNode
import cats.xml.codec.Encoder
diff --git a/docs/compiled/effect.md b/docs/compiled/effect.md
new file mode 100644
index 0000000..7749a28
--- /dev/null
+++ b/docs/compiled/effect.md
@@ -0,0 +1,5 @@
+# Cats Effect support
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-effect" % "0.0.3"
+```
diff --git a/docs/compiled/generic.md b/docs/compiled/generic.md
new file mode 100644
index 0000000..c7b2182
--- /dev/null
+++ b/docs/compiled/generic.md
@@ -0,0 +1,109 @@
+# Encoder and Decoder derivation
+
+At the moment supported only for Scala 2.
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-generic" % "0.0.3"
+```
+
+## XmlTypeInterpreter
+`XmlTypeInterpreter` is used to map fields and get the xml type and label.
+By default using `XmlTypeInterpreter.default[T]` or using it implicitly
+- `Attribute`:
+ - type is primitive
+ - type is a primitive wrapper (BigInt, BigDecimal)
+ - type is a value class
+- `Text`
+ - no fields are treated as `Text`
+- `Child`
+ - If it is not neither `Attribute` nor `Text`
+
+## Derivation
+
+Given
+```scala
+case class ValueClass(value: String) extends AnyVal
+case class Bar(field1: String, field2: BigDecimal)
+case class Foo(
+ primitiveField: Double = 666d,
+ valueClass: ValueClass,
+ bar: Bar,
+ missingField: Option[String],
+ missingNode: Option[Bar]
+)
+```
+
+### Decoder semiauto
+```scala
+import cats.xml.XmlNode
+import cats.xml.codec.Decoder
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+
+import cats.xml.syntax.*
+import cats.xml.generic.decoder.semiauto.*
+
+implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+// typeInterpreterFoo: XmlTypeInterpreter[Foo] = cats.xml.generic.XmlTypeInterpreter$$anon$1@72ff4d03
+
+implicit val decoderValueClass: Decoder[ValueClass] = deriveDecoder[ValueClass]
+// decoderValueClass: Decoder[ValueClass] = cats.xml.codec.Decoder$$anonfun$of$2@17331878
+implicit val decoderBar: Decoder[Bar] = deriveDecoder[Bar]
+// decoderBar: Decoder[Bar] = cats.xml.codec.Decoder$$anonfun$of$2@28deb4fb
+implicit val decoderFoo: Decoder[Foo] = deriveDecoder[Foo]
+// decoderFoo: Decoder[Foo] = cats.xml.codec.Decoder$$anonfun$of$2@265451b
+
+XmlNode("foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo]
+// res1: Decoder.Result[Foo] = Valid(Foo(1.0,ValueClass(TEST),Bar(BHO,100),None,None))
+```
+
+### Encoder semiauto
+```scala
+import cats.xml.codec.Encoder
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+
+import cats.xml.syntax.*
+import cats.xml.generic.encoder.semiauto.*
+
+implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+// typeInterpreterFoo: XmlTypeInterpreter[Foo] = cats.xml.generic.XmlTypeInterpreter$$anon$1@505beee
+
+implicit val encoderValueClass: Encoder[ValueClass] = deriveEncoder[ValueClass]
+// encoderValueClass: Encoder[ValueClass] = cats.xml.codec.DataEncoder$$anonfun$of$4@5821bbd9
+implicit val encoderBar: Encoder[Bar] = deriveEncoder[Bar]
+// encoderBar: Encoder[Bar] = cats.xml.codec.Encoder$$anonfun$of$2@70c4705b
+implicit val encoderFoo: Encoder[Foo] = deriveEncoder[Foo]
+// encoderFoo: Encoder[Foo] = cats.xml.codec.Encoder$$anonfun$of$2@2ed823dd
+
+Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+).toXml
+// res2: cats.xml.Xml =
+//
+//
+```
\ No newline at end of file
diff --git a/docs/compiled/standard.md b/docs/compiled/standard.md
new file mode 100644
index 0000000..bb74fae
--- /dev/null
+++ b/docs/compiled/standard.md
@@ -0,0 +1,5 @@
+# Standard Scala XML support
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-standard" % "0.0.3"
+```
diff --git a/docs/source/README.md b/docs/source/README.md
index b179355..728aca0 100644
--- a/docs/source/README.md
+++ b/docs/source/README.md
@@ -17,16 +17,22 @@ libraryDependencies += "com.github.geirolz" %% "cats-xml" % "@VERSION@"
This library is not production ready yet. There is a lot of work to do to complete it:
- ~~There are some performance issues loading xml from strings or files due the fact that there is a double
conversion, We need to parse bytes directly into `cats-xml` classes.~~
-- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
+~~- Creates macro to derive `Decoder` and `Encoder`. This is not straightforward, distinguish between a Node and an Attribute ca
can be done in some way thinking about attributes with primitives and value classes BUT distinguish between a Node/Attribute and Text
- is hard, probably an annotation or a custom Decoder/Encoder is required.
+ is hard, probably an annotation or a custom Decoder/Encoder is required.~~
- Reach a good code coverage with the tests (using munit)
- Improve documentation
- Literal macros to check XML strings at compile time
Contributions are more than welcome 💪
-Given `Foo` class
+## Modules
+- [Effect](@DOC_OUT@/effect.md)
+- [Generic](@DOC_OUT@/generic.md)
+- [Standard](@DOC_OUT@/standard.md)
+
+## Example
+Given
```scala mdoc:silent
case class Foo(
foo: Option[String],
@@ -54,7 +60,6 @@ val decoder: Decoder[Foo] =
### Encoder
-
```scala mdoc:silent
import cats.xml.XmlNode
import cats.xml.codec.Encoder
diff --git a/docs/source/effect.md b/docs/source/effect.md
new file mode 100644
index 0000000..16d0eea
--- /dev/null
+++ b/docs/source/effect.md
@@ -0,0 +1,5 @@
+# Cats Effect support
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-effect" % "@VERSION@"
+```
diff --git a/docs/source/generic.md b/docs/source/generic.md
new file mode 100644
index 0000000..148cc49
--- /dev/null
+++ b/docs/source/generic.md
@@ -0,0 +1,97 @@
+# Encoder and Decoder derivation
+
+At the moment supported only for Scala 2.
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-generic" % "@VERSION@"
+```
+
+## XmlTypeInterpreter
+`XmlTypeInterpreter` is used to map fields and get the xml type and label.
+By default using `XmlTypeInterpreter.default[T]` or using it implicitly
+- `Attribute`:
+ - type is primitive
+ - type is a primitive wrapper (BigInt, BigDecimal)
+ - type is a value class
+- `Text`
+ - no fields are treated as `Text`
+- `Child`
+ - If it is not neither `Attribute` nor `Text`
+
+## Derivation
+
+Given
+```scala mdoc:reset-object
+case class ValueClass(value: String) extends AnyVal
+case class Bar(field1: String, field2: BigDecimal)
+case class Foo(
+ primitiveField: Double = 666d,
+ valueClass: ValueClass,
+ bar: Bar,
+ missingField: Option[String],
+ missingNode: Option[Bar]
+)
+```
+
+### Decoder semiauto
+```scala mdoc:nest:to-string
+import cats.xml.XmlNode
+import cats.xml.codec.Decoder
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+
+import cats.xml.syntax.*
+import cats.xml.generic.decoder.semiauto.*
+
+implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+implicit val decoderValueClass: Decoder[ValueClass] = deriveDecoder[ValueClass]
+implicit val decoderBar: Decoder[Bar] = deriveDecoder[Bar]
+implicit val decoderFoo: Decoder[Foo] = deriveDecoder[Foo]
+
+XmlNode("foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo]
+```
+
+### Encoder semiauto
+```scala mdoc:nest:to-string
+import cats.xml.codec.Encoder
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+
+import cats.xml.syntax.*
+import cats.xml.generic.encoder.semiauto.*
+
+implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+implicit val encoderValueClass: Encoder[ValueClass] = deriveEncoder[ValueClass]
+implicit val encoderBar: Encoder[Bar] = deriveEncoder[Bar]
+implicit val encoderFoo: Encoder[Foo] = deriveEncoder[Foo]
+
+Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+).toXml
+```
\ No newline at end of file
diff --git a/docs/source/standard.md b/docs/source/standard.md
new file mode 100644
index 0000000..dad8d42
--- /dev/null
+++ b/docs/source/standard.md
@@ -0,0 +1,5 @@
+# Standard Scala XML support
+
+```sbt
+libraryDependencies += "com.github.geirolz" %% "cats-xml-standard" % "@VERSION@"
+```
diff --git a/modules/generic/example/untitled.sc b/modules/generic/example/untitled.sc
index b17f995..f76efa4 100644
--- a/modules/generic/example/untitled.sc
+++ b/modules/generic/example/untitled.sc
@@ -1,56 +1,49 @@
-import cats.xml.generic.{Value, Value1}
import cats.xml.utils.generic.TypeInfo
-//import cats.xml.codec.{Decoder, Encoder}
-//import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
-//import cats.xml.generic.decoder.auto._
-//import cats.xml.generic.encoder.auto._
-//import cats.xml.XmlNode
-//import cats.xml.codec.Decoder.Result
-//import cats.xml.implicits._
-//
-//case class Stringa(value1: String) extends AnyVal
-//
-//case class Bar(wow: String)
-//case class Foo(
-// missing: Option[String],
-// test: Stringa,
-// missingNode: Option[Bar],
-// bar: Bar
-//)
-//
-//implicit val ii: XmlTypeInterpreter[Bar] =
-// XmlTypeInterpreter
-// .withoutText[Bar]
-// .overrideType(
-// _.param(_.wow) -> XmlElemType.Text
-// )
-//
-//implicit val decBar: Decoder[Bar] = deriveDecoder[Bar]
-//val decFoo: Decoder[Foo] = deriveDecoder[Foo]
-//
-//implicit val encBar: Encoder[Bar] = deriveEncoder[Bar]
-//val encFoo: Encoder[Foo] = deriveEncoder[Foo]
-//
-//val barNode = XmlNode("bar").withText(100)
-//val fooNode = XmlNode("Foo")
-// .withAttributes("test" := "TEST")
-// .withChild(barNode)
-//
-//val decoderResult: Decoder.Result[Foo] = decFoo.decode(fooNode)
-//val encoderResult = encFoo.encode(decFoo.decode(fooNode).toOption.get)
-//// .map(encFoo.encode)
-//
-////encFoo.encode(Foo(None, "TEST", None, Bar("100")))
-//
-//
+import cats.xml.codec.{Decoder, Encoder}
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+import cats.xml.generic.decoder.auto._
+import cats.xml.generic.encoder.auto._
+import cats.xml.XmlNode
+import cats.xml.implicits._
+case class Stringa(value1: String) extends AnyVal
+case class Bar(wow: String)
+case class Foo(
+ missing: Option[String],
+ test: Stringa,
+ missingNode: Option[Bar],
+ bar: Bar
+)
+implicit val typeInfoBar: TypeInfo[Bar] = TypeInfo.deriveTypeInfo[Bar]
+
+implicit val ii: XmlTypeInterpreter[Bar] =
+ XmlTypeInterpreter
+ .withoutText[Bar]
+ .overrideType(
+ _.param(_.wow) -> XmlElemType.Text
+ )
+
+implicit val decBar: Decoder[Bar] = deriveDecoder[Bar]
+val decFoo: Decoder[Foo] = deriveDecoder[Foo]
+
+implicit val encBar: Encoder[Bar] = deriveEncoder[Bar]
+val encFoo: Encoder[Foo] = deriveEncoder[Foo]
+
+val barNode = XmlNode("bar").withText(100)
+val fooNode = XmlNode("Foo")
+ .withAttributes("test" := "TEST")
+ .withChild(barNode)
+
+val decoderResult: Decoder.Result[Foo] = decFoo.decode(fooNode)
+val encoderResult = encFoo.encode(decFoo.decode(fooNode).toOption.get)
+// .map(encFoo.encode)
+
+//encFoo.encode(Foo(None, "TEST", None, Bar("100")))
//TypeInfo.deriveTypeInfo[String]
//TypeInfo.deriveTypeInfo[Int]
-import TypeInfo.auto._
-TypeInfo.auto.deriveTypeInfo[String]
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/DecoderDerivation.scala b/modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaDecoder.scala
similarity index 54%
rename from modules/generic/src/main/scala-2/cats/xml/generic/decoder/DecoderDerivation.scala
rename to modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaDecoder.scala
index 6ba5166..c1b2fd6 100644
--- a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/DecoderDerivation.scala
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaDecoder.scala
@@ -1,18 +1,22 @@
-package cats.xml.generic.decoder
+package cats.xml.generic
+import cats.data.NonEmptyList
import cats.xml.codec.Decoder
-import cats.xml.cursor.FreeCursor
-import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
-import cats.xml.Xml
+import cats.xml.cursor.{CursorFailure, FreeCursor}
import cats.xml.utils.generic.ParamName
+import cats.xml.Xml
import magnolia1.{CaseClass, Param}
-object DecoderDerivation {
+import scala.annotation.unused
+
+object MagnoliaDecoder {
import cats.implicits.*
- // product
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Decoder, T]): Decoder[T] =
+ private[generic] def join[T: XmlTypeInterpreter](
+ ctx: CaseClass[Decoder, T],
+ @unused config: Configuration
+ ): Decoder[T] =
if (ctx.isValueClass) {
ctx.parameters.head.typeclass.map(v => ctx.rawConstruct(Seq(v)))
} else {
@@ -39,13 +43,15 @@ object DecoderDerivation {
case XmlElemType.Null => None
}
- result
-// // use fault parameter in case of missing element
-// result.map(
-// _.recoverWith(
-// useDefaultParameterIfPresentToRecoverMissing[Decoder, T, param.PType](param)
-// )
-// )
+ // use fault parameter in case of missing element
+ if (config.useDefaults)
+ result.map(
+ _.recoverWith(
+ useDefaultParameterIfPresentToRecoverMissing[Decoder, T, param.PType](param)
+ )
+ )
+ else
+ result
})
}
.toList
@@ -53,4 +59,18 @@ object DecoderDerivation {
.map(ctx.rawConstruct)
})
}
+
+ // Internal error: unable to find the outer accessor symbol of class $read
+ private def useDefaultParameterIfPresentToRecoverMissing[F[_], T, PT](
+ param: Param[F, T]
+ ): PartialFunction[NonEmptyList[CursorFailure], FreeCursor[Xml, PT]] = { failures =>
+ if (failures.forall(_.isMissing))
+ param.default match {
+ case Some(value) =>
+ FreeCursor.const[Xml, PT](value.asInstanceOf[PT].validNel[CursorFailure])
+ case None => FreeCursor.failure(failures)
+ }
+ else
+ FreeCursor.failure(failures)
+ }
}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaEncoder.scala b/modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaEncoder.scala
new file mode 100644
index 0000000..c09bf43
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/MagnoliaEncoder.scala
@@ -0,0 +1,103 @@
+package cats.xml.generic
+
+import cats.xml.{Xml, XmlAttribute, XmlData, XmlNode}
+import cats.xml.codec.Encoder
+import cats.xml.utils.generic.ParamName
+import cats.xml.Xml.XmlNull
+import magnolia1.{CaseClass, Param, SealedTrait}
+
+import scala.annotation.unused
+
+object MagnoliaEncoder {
+
+ private[generic] def join[T: XmlTypeInterpreter](
+ ctx: CaseClass[Encoder, T],
+ @unused config: Configuration
+ ): Encoder[T] = {
+ if (ctx.isValueClass) {
+ val valueParam: Param[Encoder, T] = ctx.parameters.head
+ valueParam.typeclass.contramap[T](valueParam.dereference(_))
+ } else {
+ val interpreter: XmlTypeInterpreter[T] = XmlTypeInterpreter[T]
+
+ Encoder.of(t => {
+
+ val nodeBuild = XmlNode(ctx.typeName.short)
+
+ def evaluateAndAppend(
+ xml: Xml,
+ param: Param[Encoder, T],
+ paramInfo: XmlElemTypeParamInfo
+ ): Unit =
+ xml match {
+ case XmlNull => ()
+ case data: XmlData if paramInfo.elemType == XmlElemType.Attribute =>
+ nodeBuild.mute(
+ _.appendAttr(
+ XmlAttribute(
+ key = paramInfo.labelMapper(param.label),
+ value = data
+ )
+ )
+ )
+ case data: XmlData if paramInfo.elemType == XmlElemType.Text =>
+ nodeBuild.mute(_.withText(data))
+ case node: XmlNode if paramInfo.elemType == XmlElemType.Child =>
+ nodeBuild.mute(_.appendChild(node))
+ case xml => throw new RuntimeException(debugMsg(xml, param, paramInfo))
+ }
+
+ ctx.parameters.foreach(param =>
+ interpreter
+ .evalParam(ParamName(param.label))
+ .foreach((paramInfo: XmlElemTypeParamInfo) => {
+ evaluateAndAppend(
+ xml = param.typeclass.encode(param.dereference(t)),
+ param = param,
+ paramInfo = paramInfo
+ )
+ })
+ )
+
+ nodeBuild
+ })
+ }
+ }
+
+ private[generic] def split[T: XmlTypeInterpreter](
+ sealedTrait: SealedTrait[Encoder, T],
+ @unused config: Configuration
+ ): Encoder[T] = { (a: T) =>
+ {
+ sealedTrait.split(a) { subtype =>
+ subtype.typeclass.encode(subtype.cast(a))
+ }
+ }
+ }
+
+ private def debugMsg[TC[_], T](
+ xml: Xml,
+ p: Param[TC, T],
+ paramInfo: XmlElemTypeParamInfo
+ ): String =
+ s"""
+ |Unable to handle an Xml element.
+ |
+ |Try to change your `XmlTypeInterpreter` implementation for type `${p.typeName.full}` in order to
+ |let the field `${p.label}` falls in one of the following supported cases:
+ |
+ |- XmlNode as XmlElemType.Child
+ |- XmlData as XmlElemType.Attribute
+ |- XmlData as XmlElemType.Text
+ |
+ |Current:
+ |- ${xml.getClass.getSimpleName} as ${paramInfo.elemType}
+ |
+ |---------------------------------
+ |Xml instance value: $xml
+ |Xml instance type: ${xml.getClass.getName}
+ |Field name: ${p.label}
+ |Field type: ${p.typeName.full}
+ |Treated as: ${paramInfo.elemType}
+ |""".stripMargin
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/XmlTypeInterpreter.scala b/modules/generic/src/main/scala-2/cats/xml/generic/XmlTypeInterpreter.scala
index 004030f..b1b7a99 100644
--- a/modules/generic/src/main/scala-2/cats/xml/generic/XmlTypeInterpreter.scala
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/XmlTypeInterpreter.scala
@@ -25,9 +25,8 @@ abstract class XmlTypeInterpreter[T] { $this =>
object XmlTypeInterpreter {
import cats.implicits.*
- import scala.reflect.runtime.universe.*
- def apply[T: WeakTypeTag](implicit i: XmlTypeInterpreter[T]): XmlTypeInterpreter[T] = i
+ def apply[T](implicit i: XmlTypeInterpreter[T]): XmlTypeInterpreter[T] = i
def fullOf[T: TypeInfo](
f: (ParamName[T], TypeInfo[?]) => (XmlElemType, Endo[String])
@@ -85,6 +84,18 @@ object XmlTypeInterpreter {
): XmlTypeInterpreter[T] =
XmlTypeInterpreter.fullOf[T]((label, tpe) => f.tupled.andThen(_ -> labelMapper)((label, tpe)))
+ /** By default a field is treated as Attributes if:
+ * - type is primitive
+ * - type is a primitive wrapper (BigInt, BigDecimal)
+ * - type is a value class
+ *
+ * @param textDiscriminator
+ * function to map fields as to be treated as Text
+ * @param attrsDiscriminator
+ * function to map fields as to be treated as Attribute
+ * @tparam T
+ * @return
+ */
def auto[T: TypeInfo](
textDiscriminator: (ParamName[T], TypeInfo[?]) => Boolean,
attrsDiscriminator: (ParamName[T], TypeInfo[?]) => Boolean =
@@ -92,9 +103,7 @@ object XmlTypeInterpreter {
tpeInfo.isString
|| tpeInfo.isPrimitive
|| tpeInfo.isPrimitiveWrapper
- || tpeInfo.hasArgsTypePrimitive
- || tpeInfo.hasArgsTypeOfString
- || tpeInfo.isValueClassOfPrimitivesOrString
+ || tpeInfo.isValueClass
): XmlTypeInterpreter[T] =
XmlTypeInterpreter.of[T] { case (paramName, tpeInfo) =>
if (textDiscriminator(paramName, tpeInfo))
@@ -119,6 +128,6 @@ object XmlTypeInterpreter {
.contains(paramName)
)
- implicit def defaultWithoutText[T: TypeInfo]: XmlTypeInterpreter[T] =
+ implicit def default[T: TypeInfo]: XmlTypeInterpreter[T] =
XmlTypeInterpreter.withoutText[T]
}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/auto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/auto.scala
new file mode 100644
index 0000000..bd86a88
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/auto.scala
@@ -0,0 +1,16 @@
+package cats.xml.generic.decoder
+
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Configuration, MagnoliaDecoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia}
+
+object auto {
+
+ type Typeclass[T] = Decoder[T]
+
+ implicit def deriveDecoder[T]: Typeclass[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](ctx: CaseClass[Typeclass, T]): Typeclass[T] =
+ MagnoliaDecoder.join(ctx, Configuration.default)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/auto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/auto.scala
new file mode 100644
index 0000000..cbe0953
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/auto.scala
@@ -0,0 +1,18 @@
+package cats.xml.generic.decoder.configured
+
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Configuration, MagnoliaDecoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia}
+
+object auto {
+
+ type Typeclass[T] = Decoder[T]
+
+ implicit def deriveConfiguredDecoder[T]: Typeclass[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](ctx: CaseClass[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaDecoder.join(ctx, config)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/semiauto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/semiauto.scala
new file mode 100644
index 0000000..455ff8a
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/configured/semiauto.scala
@@ -0,0 +1,18 @@
+package cats.xml.generic.decoder.configured
+
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Configuration, MagnoliaDecoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia}
+
+object semiauto {
+
+ type Typeclass[T] = Decoder[T]
+
+ def deriveConfiguredDecoder[T]: Typeclass[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](ctx: CaseClass[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaDecoder.join(ctx, config)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/macros.scala b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/macros.scala
deleted file mode 100644
index 966b9da..0000000
--- a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/macros.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package cats.xml.generic.decoder
-
-import cats.xml.codec.Decoder
-import cats.xml.generic.XmlTypeInterpreter
-import magnolia1.*
-
-object auto {
-
- type Typeclass[T] = Decoder[T]
-
- implicit def deriveDecoder[T]: Decoder[T] =
- macro Magnolia.gen[T]
-
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Decoder, T]): Decoder[T] =
- DecoderDerivation.join(ctx)
-}
-
-object semiauto {
-
- type Typeclass[T] = Decoder[T]
-
- def deriveDecoder[T]: Decoder[T] =
- macro Magnolia.gen[T]
-
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Decoder, T]): Decoder[T] =
- DecoderDerivation.join(ctx)
-}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/decoder/semiauto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/semiauto.scala
new file mode 100644
index 0000000..0010649
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/decoder/semiauto.scala
@@ -0,0 +1,16 @@
+package cats.xml.generic.decoder
+
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Configuration, MagnoliaDecoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia}
+
+object semiauto {
+
+ type Typeclass[T] = Decoder[T]
+
+ def deriveDecoder[T]: Typeclass[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](ctx: CaseClass[Typeclass, T]): Typeclass[T] =
+ MagnoliaDecoder.join(ctx, Configuration.default)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/EncoderDerivation.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/EncoderDerivation.scala
deleted file mode 100644
index 25c16d9..0000000
--- a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/EncoderDerivation.scala
+++ /dev/null
@@ -1,47 +0,0 @@
-package cats.xml.generic.encoder
-
-import cats.xml.{XmlAttribute, XmlData, XmlNode}
-import cats.xml.codec.Encoder
-import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
-import cats.xml.Xml.XmlNull
-import cats.xml.utils.generic.ParamName
-import magnolia1.{CaseClass, Param}
-
-object EncoderDerivation {
-
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Encoder, T]): Encoder[T] = {
- if (ctx.isValueClass) {
- val valueParam: Param[Encoder, T] = ctx.parameters.head
- valueParam.typeclass.contramap[T](valueParam.dereference(_))
- } else {
- val interpreter: XmlTypeInterpreter[T] = XmlTypeInterpreter[T]
- Encoder.of(t => {
- val nodeBuild = XmlNode(ctx.typeName.short)
- ctx.parameters.foreach(p =>
- interpreter
- .evalParam(ParamName(p.label))
- .foreach(paramInfo => {
- p.typeclass.encode(p.dereference(t)) match {
- case XmlNull => ()
- case data: XmlData if paramInfo.elemType == XmlElemType.Attribute =>
- nodeBuild.mute(
- _.appendAttr(
- XmlAttribute(
- key = paramInfo.labelMapper(p.label),
- value = data
- )
- )
- )
- case data: XmlData if paramInfo.elemType == XmlElemType.Text =>
- nodeBuild.mute(_.withText(data))
- case node: XmlNode => nodeBuild.mute(_.appendChild(node))
- case _ => ()
- }
- })
- )
-
- nodeBuild
- })
- }
- }
-}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/auto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/auto.scala
new file mode 100644
index 0000000..24a8613
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/auto.scala
@@ -0,0 +1,18 @@
+package cats.xml.generic.encoder
+
+import cats.xml.codec.Encoder
+import cats.xml.generic.{Configuration, MagnoliaEncoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia, SealedTrait}
+
+object auto {
+
+ type Typeclass[T] = Encoder[T]
+
+ implicit def deriveEncoder[T]: Typeclass[T] = macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](caseClass: CaseClass[Typeclass, T]): Typeclass[T] =
+ MagnoliaEncoder.join(caseClass, Configuration.default)
+
+ def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] =
+ MagnoliaEncoder.split(sealedTrait, Configuration.default)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/auto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/auto.scala
new file mode 100644
index 0000000..f117138
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/auto.scala
@@ -0,0 +1,22 @@
+package cats.xml.generic.encoder.configured
+
+import cats.xml.codec.Encoder
+import cats.xml.generic.{Configuration, MagnoliaEncoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia, SealedTrait}
+
+object auto {
+
+ type Typeclass[T] = Encoder[T]
+
+ implicit def deriveConfiguredEncoder[T]: Typeclass[T] = macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](caseClass: CaseClass[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaEncoder.join(caseClass, config)
+
+ def split[T](sealedTrait: SealedTrait[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaEncoder.split(sealedTrait, config)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/semiauto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/semiauto.scala
new file mode 100644
index 0000000..cb4e858
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/configured/semiauto.scala
@@ -0,0 +1,23 @@
+package cats.xml.generic.encoder.configured
+
+import cats.xml.codec.Encoder
+import cats.xml.generic.{Configuration, MagnoliaEncoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia, SealedTrait}
+
+object semiauto {
+
+ type Typeclass[T] = Encoder[T]
+
+ def deriveConfiguredEncoder[T]: Encoder[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](caseClass: CaseClass[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaEncoder.join(caseClass, config)
+
+ def split[T](sealedTrait: SealedTrait[Typeclass, T])(implicit
+ config: Configuration
+ ): Typeclass[T] =
+ MagnoliaEncoder.split(sealedTrait, config)
+}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/macros.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/macros.scala
deleted file mode 100644
index 7a97763..0000000
--- a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/macros.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package cats.xml.generic.encoder
-
-import cats.xml.codec.Encoder
-import cats.xml.generic.XmlTypeInterpreter
-import magnolia1.{CaseClass, Magnolia}
-
-object auto {
-
- type Typeclass[T] = Encoder[T]
-
- implicit def deriveEncoder[T]: Encoder[T] =
- macro Magnolia.gen[T]
-
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Encoder, T]): Encoder[T] =
- EncoderDerivation.join(ctx)
-}
-
-object semiauto {
-
- type Typeclass[T] = Encoder[T]
-
- def deriveEncoder[T]: Encoder[T] =
- macro Magnolia.gen[T]
-
- def join[T: XmlTypeInterpreter](ctx: CaseClass[Encoder, T]): Encoder[T] =
- EncoderDerivation.join(ctx)
-}
diff --git a/modules/generic/src/main/scala-2/cats/xml/generic/encoder/semiauto.scala b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/semiauto.scala
new file mode 100644
index 0000000..0111bcd
--- /dev/null
+++ b/modules/generic/src/main/scala-2/cats/xml/generic/encoder/semiauto.scala
@@ -0,0 +1,19 @@
+package cats.xml.generic.encoder
+
+import cats.xml.codec.Encoder
+import cats.xml.generic.{Configuration, MagnoliaEncoder, XmlTypeInterpreter}
+import magnolia1.{CaseClass, Magnolia, SealedTrait}
+
+object semiauto {
+
+ type Typeclass[T] = Encoder[T]
+
+ def deriveEncoder[T]: Encoder[T] =
+ macro Magnolia.gen[T]
+
+ def join[T: XmlTypeInterpreter](caseClass: CaseClass[Typeclass, T]): Typeclass[T] =
+ MagnoliaEncoder.join(caseClass, Configuration.default)
+
+ def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] =
+ MagnoliaEncoder.split(sealedTrait, Configuration.default)
+}
diff --git a/modules/generic/src/main/scala/cats/xml/generic/Configuration.scala b/modules/generic/src/main/scala/cats/xml/generic/Configuration.scala
new file mode 100644
index 0000000..baf6391
--- /dev/null
+++ b/modules/generic/src/main/scala/cats/xml/generic/Configuration.scala
@@ -0,0 +1,20 @@
+package cats.xml.generic
+
+final case class Configuration(
+ useDefaults: Boolean,
+ discriminator: Option[String]
+) {
+
+ def withDefaults: Configuration =
+ copy(useDefaults = true)
+
+ def withDiscriminator(discriminator: String): Configuration =
+ copy(discriminator = Some(discriminator))
+}
+object Configuration {
+
+ val default: Configuration = Configuration(
+ useDefaults = false,
+ discriminator = None
+ )
+}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/Samples.scala b/modules/generic/src/test/scala-2/cats/xml/generic/Samples.scala
new file mode 100644
index 0000000..e1633d1
--- /dev/null
+++ b/modules/generic/src/test/scala-2/cats/xml/generic/Samples.scala
@@ -0,0 +1,15 @@
+package cats.xml.generic
+
+object Samples {
+ case class ValueClass(value: String) extends AnyVal
+
+ case class Bar(field1: String, field2: BigDecimal)
+
+ case class Foo(
+ primitiveField: Double = 666d,
+ valueClass: ValueClass,
+ bar: Bar,
+ missingField: Option[String],
+ missingNode: Option[Bar]
+ )
+}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/decoder/DecoderSuite.scala b/modules/generic/src/test/scala-2/cats/xml/generic/decoder/DecoderSuite.scala
new file mode 100644
index 0000000..b781c9f
--- /dev/null
+++ b/modules/generic/src/test/scala-2/cats/xml/generic/decoder/DecoderSuite.scala
@@ -0,0 +1,95 @@
+package cats.xml.generic.decoder
+
+import cats.data.Validated.Valid
+import cats.xml.XmlNode
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Samples, XmlElemType, XmlTypeInterpreter}
+
+class DecoderSuite extends munit.FunSuite {
+
+ import cats.xml.syntax.*
+ import Samples.*
+
+ test("auto") {
+
+ import cats.xml.generic.decoder.auto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val decoderBar: Decoder[Bar] = deriveDecoder[Bar]
+ implicit val decoderFoo: Decoder[Foo] = deriveDecoder[Foo]
+
+ assertEquals(
+ obtained = XmlNode("foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo],
+ expected = Valid(
+ Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ )
+ )
+ )
+ }
+
+ test("semiauto") {
+
+ import cats.xml.generic.decoder.semiauto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val decoderValueClass: Decoder[ValueClass] = deriveDecoder[ValueClass]
+ implicit val decoderBar: Decoder[Bar] = deriveDecoder[Bar]
+ implicit val decoderFoo: Decoder[Foo] = deriveDecoder[Foo]
+
+ assertEquals(
+ obtained = XmlNode("foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo],
+ expected = Valid(
+ Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ )
+ )
+ )
+
+ }
+
+}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/decoder/configured/ConfiguredDecoderSuite.scala b/modules/generic/src/test/scala-2/cats/xml/generic/decoder/configured/ConfiguredDecoderSuite.scala
new file mode 100644
index 0000000..aeaa755
--- /dev/null
+++ b/modules/generic/src/test/scala-2/cats/xml/generic/decoder/configured/ConfiguredDecoderSuite.scala
@@ -0,0 +1,3 @@
+package cats.xml.generic.decoder.configured
+
+class ConfiguredDecoderSuite extends munit.FunSuite {}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/encoder/EncoderSuite.scala b/modules/generic/src/test/scala-2/cats/xml/generic/encoder/EncoderSuite.scala
new file mode 100644
index 0000000..d6d7347
--- /dev/null
+++ b/modules/generic/src/test/scala-2/cats/xml/generic/encoder/EncoderSuite.scala
@@ -0,0 +1,87 @@
+package cats.xml.generic.encoder
+
+import cats.xml.XmlNode
+import cats.xml.codec.Encoder
+import cats.xml.generic.{XmlElemType, XmlTypeInterpreter}
+
+class EncoderSuite extends munit.FunSuite {
+
+ import cats.xml.syntax.*
+ import cats.xml.generic.Samples.*
+
+ test("auto") {
+
+ import cats.xml.generic.encoder.auto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val encoderBar: Encoder[Bar] = deriveEncoder[Bar]
+ implicit val encoderFoo: Encoder[Foo] = deriveEncoder[Foo]
+
+ assertEquals(
+ obtained = Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ ).toXml,
+ expected = XmlNode("Foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("Bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ )
+ }
+
+ test("semiauto") {
+
+ import cats.xml.generic.encoder.semiauto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val encoderValueClass: Encoder[ValueClass] = deriveEncoder[ValueClass]
+ implicit val encoderBar: Encoder[Bar] = deriveEncoder[Bar]
+ implicit val encoderFoo: Encoder[Foo] = deriveEncoder[Foo]
+
+ assertEquals(
+ obtained = Foo(
+ primitiveField = 1d,
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ ).toXml,
+ expected = XmlNode("Foo")
+ .withAttributes(
+ "primitiveField" := 1d,
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("Bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ )
+ }
+
+}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/encoder/GenericEncoderSuite.scala b/modules/generic/src/test/scala-2/cats/xml/generic/encoder/GenericEncoderSuite.scala
deleted file mode 100644
index a5018c2..0000000
--- a/modules/generic/src/test/scala-2/cats/xml/generic/encoder/GenericEncoderSuite.scala
+++ /dev/null
@@ -1,53 +0,0 @@
-package cats.xml.generic.encoder
-
-import cats.xml.XmlNode
-import cats.xml.codec.Encoder
-import cats.xml.utils.generic.TypeInfo
-
-case class ValueClass(value: String) extends AnyVal
-case class Bar(field1: String, field2: BigDecimal)
-case class Foo(
- primitiveField: Double,
- valueClass: ValueClass,
- bar: Bar,
- missingField: Option[String],
- missingNode: Option[Bar]
-)
-
-class GenericEncoderSuite extends munit.FunSuite {
-
- import cats.xml.syntax.*
-
- test("auto") {
-
- import cats.xml.generic.encoder.auto.*
-
- implicit val t1: TypeInfo[Bar] = TypeInfo.auto.deriveTypeInfo[Bar]
- implicit val t2: TypeInfo[Foo] = TypeInfo.auto.deriveTypeInfo[Foo]
-
- implicit val encoderBar: Encoder[Bar] = deriveEncoder[Bar]
- implicit val encoderFoo: Encoder[Foo] = deriveEncoder[Foo]
-
- assertEquals(
- obtained = Foo(
- primitiveField = 1d,
- valueClass = ValueClass("TEST"),
- bar = Bar("BHO", BigDecimal(100)),
- missingField = None,
- missingNode = None
- ).toXml,
- expected = XmlNode("Foo")
- .withAttributes(
- "primitiveField" := 1d,
- "valueClass" := "TEST"
- )
- .withChild(
- XmlNode("Bar")
- .withAttributes(
- "field1" := "BHO",
- "field2" := BigDecimal(100)
- )
- )
- )
- }
-}
diff --git a/modules/generic/src/test/scala-2/cats/xml/generic/encoder/configured/ConfiguredDecoderSuite.scala b/modules/generic/src/test/scala-2/cats/xml/generic/encoder/configured/ConfiguredDecoderSuite.scala
new file mode 100644
index 0000000..3bdd760
--- /dev/null
+++ b/modules/generic/src/test/scala-2/cats/xml/generic/encoder/configured/ConfiguredDecoderSuite.scala
@@ -0,0 +1,95 @@
+package cats.xml.generic.encoder.configured
+
+import cats.data.Validated.Valid
+import cats.xml.XmlNode
+import cats.xml.codec.Decoder
+import cats.xml.generic.{Configuration, XmlElemType, XmlTypeInterpreter}
+
+class ConfiguredDecoderSuite extends munit.FunSuite {
+
+ import cats.xml.generic.Samples.*
+ import cats.xml.syntax.*
+
+ test("configured.auto") {
+
+ import cats.xml.generic.decoder.configured.auto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val config: Configuration = Configuration.default.withDefaults
+ implicit val decoderBar: Decoder[Bar] = deriveConfiguredDecoder[Bar]
+ implicit val decoderFoo: Decoder[Foo] = deriveConfiguredDecoder[Foo]
+
+ assertEquals(
+ obtained = XmlNode("foo")
+ .withAttributes(
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo],
+ expected = Valid(
+ Foo(
+ primitiveField = 666d, // default value
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ )
+ )
+ )
+ }
+
+ test("configured.semiauto") {
+
+ import cats.xml.generic.decoder.configured.semiauto.*
+
+ implicit val typeInterpreterFoo: XmlTypeInterpreter[Foo] =
+ XmlTypeInterpreter
+ .default[Foo]
+ .overrideType(
+ _.param(_.valueClass) -> XmlElemType.Attribute
+ )
+
+ implicit val config: Configuration = Configuration.default.withDefaults
+ implicit val decoderValueClass: Decoder[ValueClass] = deriveConfiguredDecoder[ValueClass]
+ implicit val decoderBar: Decoder[Bar] = deriveConfiguredDecoder[Bar]
+ implicit val decoderFoo: Decoder[Foo] = deriveConfiguredDecoder[Foo]
+
+ assertEquals(
+ obtained = XmlNode("foo")
+ .withAttributes(
+ "valueClass" := "TEST"
+ )
+ .withChild(
+ XmlNode("bar")
+ .withAttributes(
+ "field1" := "BHO",
+ "field2" := BigDecimal(100)
+ )
+ )
+ .as[Foo],
+ expected = Valid(
+ Foo(
+ primitiveField = 666d, // default value
+ valueClass = ValueClass("TEST"),
+ bar = Bar("BHO", BigDecimal(100)),
+ missingField = None,
+ missingNode = None
+ )
+ )
+ )
+
+ }
+
+}
diff --git a/modules/standard/src/main/scala/cats/xml/std/NodeSeqConverter.scala b/modules/standard/src/main/scala/cats/xml/std/NodeSeqConverter.scala
index aac8cd2..ed89f7a 100644
--- a/modules/standard/src/main/scala/cats/xml/std/NodeSeqConverter.scala
+++ b/modules/standard/src/main/scala/cats/xml/std/NodeSeqConverter.scala
@@ -2,7 +2,6 @@ package cats.xml.std
import cats.xml.*
import cats.Eq
-import cats.xml.XmlData.*
import scala.annotation.{tailrec, unused}
import scala.xml.*
@@ -19,7 +18,7 @@ private[std] object NodeSeqConverter extends NodeSeqConverterInstances with Node
XmlNode(
label = e.label,
attributes = XmlAttribute.fromMetaData(e.attributes),
- content = NodeContent.Text(XmlString(e.text.trim))
+ content = NodeContent.Text(Xml.Data.fromString(e.text.trim))
)
case e: Elem =>
val tree = XmlNode(
diff --git a/modules/standard/src/main/scala/cats/xml/std/XmlAttributeConverter.scala b/modules/standard/src/main/scala/cats/xml/std/XmlAttributeConverter.scala
index eed1264..6f0c53c 100644
--- a/modules/standard/src/main/scala/cats/xml/std/XmlAttributeConverter.scala
+++ b/modules/standard/src/main/scala/cats/xml/std/XmlAttributeConverter.scala
@@ -1,7 +1,6 @@
package cats.xml.std
-import cats.xml.XmlAttribute
-import cats.xml.XmlData.*
+import cats.xml.{Xml, XmlAttribute}
import scala.annotation.unused
import scala.xml.{MetaData, Null}
@@ -9,7 +8,7 @@ import scala.xml.{MetaData, Null}
private[std] object XmlAttributeConverter {
def fromMetaData(metaData: MetaData): List[XmlAttribute] =
- metaData.iterator.map(m => XmlAttribute(m.key, XmlString(m.value.text))).toList
+ metaData.iterator.map(m => XmlAttribute(m.key, Xml.Data.fromString(m.value.text))).toList
def toMetaData(attr: XmlAttribute): MetaData =
new scala.xml.UnprefixedAttribute(attr.key, attr.value.toString, Null)
diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala
index 4ac9694..d0e2f63 100644
--- a/project/ProjectDependencies.scala
+++ b/project/ProjectDependencies.scala
@@ -9,15 +9,13 @@ object ProjectDependencies {
lazy val common: Seq[ModuleID] = Seq(
// SCALA
- "org.typelevel" %% "cats-core" % "2.8.0" cross CrossVersion.binary,
-// "org.typelevel" %% "mouse" % "1.0.10",
-// "org.scala-lang" % "scala-compiler" % "2.13.8",
+ "org.typelevel" %% "cats-core" % "2.8.0",
// TEST
"org.scalameta" %% "munit" % "0.7.29" % Test,
"org.scalameta" %% "munit-scalacheck" % "0.7.29" % Test,
"org.typelevel" %% "cats-laws" % "2.8.0" % Test,
"org.typelevel" %% "discipline-munit" % "1.0.9" % Test,
- "org.scalacheck" %% "scalacheck" % "1.16.0" % Test cross CrossVersion.binary
+ "org.scalacheck" %% "scalacheck" % "1.16.0" % Test
)
object Docs {
@@ -26,8 +24,7 @@ object ProjectDependencies {
object Utils {
val dedicated: Seq[ModuleID] = List(
- "org.scala-lang" % "scala-reflect" % "2.13.8",
- "com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.2"
+ "org.scala-lang" % "scala-reflect" % "2.13.8"
)
}
@@ -42,20 +39,20 @@ object ProjectDependencies {
"com.chuusai" %% "shapeless" % "2.3.9"
)
val scala3: Seq[ModuleID] = Seq(
- "com.softwaremill.magnolia1_3" %% "magnolia" % "1.1.1"
+ "com.softwaremill.magnolia1_3" %% "magnolia" % "1.1.5"
)
}
object Effect {
val dedicated: Seq[ModuleID] = Seq(
- "org.typelevel" %% "cats-effect" % "3.3.14" cross CrossVersion.binary,
+ "org.typelevel" %% "cats-effect" % "3.3.14",
"org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test
)
}
object Standard {
val dedicated: Seq[ModuleID] = Seq(
- "org.scala-lang.modules" %% "scala-xml" % "2.1.0" cross CrossVersion.binary
+ "org.scala-lang.modules" %% "scala-xml" % "2.1.0"
)
}
diff --git a/utils/src/main/example/untitled.sc b/utils/src/main/example/untitled.sc
index 4697b7b..3fba379 100644
--- a/utils/src/main/example/untitled.sc
+++ b/utils/src/main/example/untitled.sc
@@ -1,7 +1,8 @@
import cats.xml.utils.generic.TypeInfo
import cats.xml.utils.generic._
-val t: TypeInfo[Foo] = TypeInfo.auto.deriveTypeInfo[Foo]
+
+val t: TypeInfo[Foo] = TypeInfo.deriveTypeInfo[Foo]
//val t: Map[ParamName[Foo], TypeInfo[_]] = TypeInfo.auto.deriveFieldsTypeInfo[Foo]
diff --git a/utils/src/main/scala-2/cats/xml/utils/generic/TypeInfo.scala b/utils/src/main/scala-2/cats/xml/utils/generic/TypeInfo.scala
deleted file mode 100644
index 6457147..0000000
--- a/utils/src/main/scala-2/cats/xml/utils/generic/TypeInfo.scala
+++ /dev/null
@@ -1,141 +0,0 @@
-package cats.xml.utils.generic
-
-import cats.Show
-
-import scala.reflect.macros.blackbox
-
-case class TypeInfo[T] private (
- isString: Boolean,
- isPrimitiveWrapper: Boolean,
- isPrimitive: Boolean,
- hasArgsTypePrimitive: Boolean,
- hasArgsTypeOfString: Boolean,
- isValueClass: Boolean,
- isValueClassOfPrimitivesOrString: Boolean,
- accessorsInfo: Map[ParamName[T], TypeInfo[?]]
-) {
- override def toString: String = Show[TypeInfo[T]].show(this)
-}
-object TypeInfo {
-
- def apply[T: TypeInfo]: TypeInfo[T] = implicitly[TypeInfo[T]]
-
- def of[T](
- isString: Boolean,
- isPrimitiveWrapper: Boolean,
- isPrimitive: Boolean,
- hasArgsTypePrimitive: Boolean,
- hasArgsTypeOfString: Boolean,
- isValueClass: Boolean,
- isValueClassOfPrimitivesOrString: Boolean,
- accessorsInfo: Map[ParamName[T], TypeInfo[?]]
- ): TypeInfo[T] = new TypeInfo[T](
- isString,
- isPrimitiveWrapper,
- isPrimitive,
- hasArgsTypePrimitive,
- hasArgsTypeOfString,
- isValueClass,
- isValueClassOfPrimitivesOrString,
- accessorsInfo
- )
-
- object auto {
- implicit def deriveTypeInfo[T]: TypeInfo[T] =
- macro TypeInfoMacros.deriveTypeInfoImpl[T]
-
- implicit def deriveFieldsTypeInfo[T]: Map[ParamName[T], TypeInfo[?]] =
- macro TypeInfoMacros.deriveFieldsTypeInfoImpl[T]
- }
-
- implicit def showTypeInfo[T]: Show[TypeInfo[T]] =
- (t: TypeInfo[T]) => s"""
- |isString: ${t.isString}
- |isPrimitive: ${t.isPrimitive}
- |hasArgsTypePrimitive: ${t.hasArgsTypePrimitive}
- |hasArgsTypeOfString: ${t.hasArgsTypeOfString}
- |isValueClass: ${t.isValueClass}
- |isValueClassOfPrimitivesOrString: ${t.isValueClassOfPrimitivesOrString}
- |accessorsInfo: ${t.accessorsInfo}
- |""".stripMargin
-}
-
-object TypeInfoMacros {
-
- def deriveTypeInfoImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[TypeInfo[T]] = {
- import c.universe.*
-
- val wtpe = weakTypeOf[T]
-
- // primitive
- def isPrimitive(tpe: c.universe.Type) =
- tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isPrimitive
-
- def isPrimitiveWrapper(tpe: c.universe.Type) =
- List(
- weakTypeOf[BigDecimal],
- weakTypeOf[BigInt]
- ).exists(pWrapperTpe => tpe <:< pWrapperTpe)
-
- // value class
- def isValueClass(tpe: c.universe.Type): Boolean =
- tpe <:< typeOf[AnyVal] &&
- tpe.typeSymbol.isClass &&
- tpe.typeSymbol.asClass.isCaseClass &&
- getAccessors(tpe).size == 1
-
- def isValueClassOfPrimitivesOrString(tpe: c.universe.Type): Boolean = {
- isValueClass(tpe)
- && getAccessors(tpe).headOption.exists(ptpe => {
- isPrimitive(ptpe.info) || isPrimitiveWrapper(ptpe.info)
- })
- }
-
- // utils
- def getAccessors(tpe: c.universe.Type): Iterable[c.universe.MethodSymbol] =
- tpe.members.collect {
- case m: MethodSymbol if m.isGetter && m.isPublic => m
- }
-
- c.Expr[TypeInfo[T]](
- q"""
- import cats.xml.utils.generic.TypeInfo
- import cats.xml.utils.generic.*
- import scala.reflect.runtime.universe.*
-
- TypeInfo.of[${wtpe.typeSymbol}](
- isString = ${wtpe <:< weakTypeOf[String]},
- isPrimitiveWrapper = ${isPrimitiveWrapper(wtpe)},
- isPrimitive = ${isPrimitive(wtpe)},
- hasArgsTypePrimitive = false,
- hasArgsTypeOfString = false,
- isValueClass = ${isValueClass(wtpe)},
- isValueClassOfPrimitivesOrString = ${isValueClassOfPrimitivesOrString(wtpe)},
- accessorsInfo = ${deriveFieldsTypeInfoImpl[T](c)}
- )
- """
- )
- }
-
- def deriveFieldsTypeInfoImpl[T: c.WeakTypeTag](
- c: blackbox.Context
- ): c.Expr[Map[ParamName[T], TypeInfo[?]]] = {
- import c.universe.*
-
- val wtpe = weakTypeOf[T]
- val tuples: List[Tree] = wtpe.members.collect {
- case mSymbol: MethodSymbol if mSymbol.isGetter && mSymbol.isPublic =>
- val name = mSymbol.name.toString
- q"""
- import cats.xml.utils.generic.TypeInfo
- import cats.xml.utils.generic.*
- (ParamName[${wtpe.typeSymbol}]($name), TypeInfo.auto.deriveTypeInfo[${mSymbol.returnType.typeSymbol}])
- """
- }.toList
-
- c.Expr[Map[ParamName[T], TypeInfo[?]]](
- q"""List(..$tuples).toMap"""
- )
- }
-
-}
diff --git a/utils/src/main/scala-2/cats/xml/utils/generic/typeInfoInstances.scala b/utils/src/main/scala-2/cats/xml/utils/generic/typeInfoInstances.scala
new file mode 100644
index 0000000..08910eb
--- /dev/null
+++ b/utils/src/main/scala-2/cats/xml/utils/generic/typeInfoInstances.scala
@@ -0,0 +1,95 @@
+package cats.xml.utils.generic
+
+import scala.reflect.macros.blackbox
+
+trait TypeInfoInstances {
+ implicit def deriveTypeInfo[T]: TypeInfo[T] =
+ macro TypeInfoMacros.deriveTypeInfoImpl[T]
+
+ implicit def deriveFieldsTypeInfo[T]: Map[ParamName[T], TypeInfo[?]] =
+ macro TypeInfoMacros.deriveFieldsTypeInfoImpl[T]
+}
+object TypeInfoMacros {
+
+ def deriveTypeInfoImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[TypeInfo[T]] = {
+ import c.universe.*
+
+ val wtpe: c.universe.Type = weakTypeOf[T].finalResultType
+ val utils: BlackboxTypesUtils[c.type] = new BlackboxTypesUtils(c)
+ val isString: Boolean = wtpe <:< weakTypeOf[String]
+ val isPrimitiveWrapper: Boolean = utils.isPrimitiveWrapper(wtpe)
+ val isPrimitive: Boolean = utils.isPrimitive(wtpe)
+ val isValueClass: Boolean = utils.isValueClass(wtpe)
+ val accessorsInfo: c.Expr[Map[ParamName[T], TypeInfo[?]]] = deriveFieldsTypeInfoImpl[T](c)
+
+ c.Expr[TypeInfo[T]](
+ q"""
+ import cats.xml.utils.generic.TypeInfo
+ import cats.xml.utils.generic.*
+ import scala.reflect.runtime.universe.*
+
+ TypeInfo.of[$wtpe](
+ isString = $isString,
+ isPrimitiveWrapper = $isPrimitiveWrapper,
+ isPrimitive = $isPrimitive,
+ isValueClass = $isValueClass,
+ accessorsInfo = $accessorsInfo
+ )
+ """
+ )
+ }
+
+ def deriveFieldsTypeInfoImpl[T: c.WeakTypeTag](
+ c: blackbox.Context
+ ): c.Expr[Map[ParamName[T], TypeInfo[?]]] = {
+ import c.universe.*
+
+ val wtpe = weakTypeOf[T].finalResultType
+
+ val tuples: List[Tree] = wtpe.members.collect {
+ case mSymbol: MethodSymbol if mSymbol.isGetter && mSymbol.isPublic =>
+ val name = mSymbol.name.toString
+ q"""
+ (ParamName[$wtpe]($name), TypeInfo.deriveTypeInfo[${mSymbol.returnType}])
+ """
+ }.toList
+
+ c.Expr[Map[ParamName[T], TypeInfo[?]]](
+ q"""
+ import cats.xml.utils.generic.TypeInfo
+ import cats.xml.utils.generic.*
+
+ List(..$tuples).toMap
+ """
+ )
+ }
+
+ private class BlackboxTypesUtils[C <: blackbox.Context](val c: C) {
+
+ import c.universe.*
+
+ // primitive
+ def isPrimitive(tpe: c.universe.Type): Boolean =
+ tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isPrimitive
+
+ def isPrimitiveWrapper(tpe: c.universe.Type): Boolean =
+ List(
+ weakTypeOf[BigDecimal],
+ weakTypeOf[BigInt]
+ ).exists(pWrapperTpe => tpe <:< pWrapperTpe)
+
+ // value class
+ def isValueClass(tpe: c.universe.Type): Boolean =
+ tpe <:< typeOf[AnyVal] &&
+ tpe.typeSymbol.isClass &&
+ tpe.typeSymbol.asClass.isCaseClass &&
+ getAccessors(tpe).size == 1
+
+ // utils
+ def getAccessors(tpe: c.universe.Type): Iterable[c.universe.MethodSymbol] =
+ tpe.members.collect {
+ case m: MethodSymbol if m.isGetter && m.isPublic => m
+ }
+ }
+
+}
diff --git a/utils/src/main/scala-3/cats/xml/utils/generic/typeInfoInstances.scala b/utils/src/main/scala-3/cats/xml/utils/generic/typeInfoInstances.scala
new file mode 100644
index 0000000..da0f880
--- /dev/null
+++ b/utils/src/main/scala-3/cats/xml/utils/generic/typeInfoInstances.scala
@@ -0,0 +1,5 @@
+package cats.xml.utils.generic
+
+import scala.reflect.macros.blackbox
+
+trait TypeInfoInstances {}
diff --git a/core/src/main/scala/cats/xml/utils/BooleanUtils.scala b/utils/src/main/scala/cats/xml/utils/BooleanUtils.scala
similarity index 100%
rename from core/src/main/scala/cats/xml/utils/BooleanUtils.scala
rename to utils/src/main/scala/cats/xml/utils/BooleanUtils.scala
diff --git a/core/src/main/scala/cats/xml/utils/ErrorKeeper.scala b/utils/src/main/scala/cats/xml/utils/UnderlyingThrowable.scala
similarity index 51%
rename from core/src/main/scala/cats/xml/utils/ErrorKeeper.scala
rename to utils/src/main/scala/cats/xml/utils/UnderlyingThrowable.scala
index 18a5e2e..875a99f 100644
--- a/core/src/main/scala/cats/xml/utils/ErrorKeeper.scala
+++ b/utils/src/main/scala/cats/xml/utils/UnderlyingThrowable.scala
@@ -2,19 +2,19 @@ package cats.xml.utils
import cats.Eq
-trait ErrorKeeper {
+trait UnderlyingThrowable {
val error: Throwable
override def equals(obj: Any): Boolean =
obj match {
- case keeper: ErrorKeeper => Eq[ErrorKeeper].eqv(this, keeper)
- case _ => false
+ case keeper: UnderlyingThrowable => Eq[UnderlyingThrowable].eqv(this, keeper)
+ case _ => false
}
}
-object ErrorKeeper {
- implicit val eqErrorKeeper: Eq[ErrorKeeper] =
- (x: ErrorKeeper, y: ErrorKeeper) =>
+object UnderlyingThrowable {
+ implicit val weakEqUnderlyingThrowable: Eq[UnderlyingThrowable] =
+ (x: UnderlyingThrowable, y: UnderlyingThrowable) =>
x.error == y.error || (
x.error.getClass.isAssignableFrom(y.error.getClass) &&
x.error.getCause == y.error.getCause &&
diff --git a/utils/src/main/scala/cats/xml/utils/format/Indentator.scala b/utils/src/main/scala/cats/xml/utils/format/Indentator.scala
new file mode 100644
index 0000000..46e2efe
--- /dev/null
+++ b/utils/src/main/scala/cats/xml/utils/format/Indentator.scala
@@ -0,0 +1,36 @@
+package cats.xml.utils.format
+
+case class Indentator(char: Char, size: Int, depth: Int, indentation: String) {
+
+ private val unit = Indentator.buildString(char, size, 1)
+
+ def forward: Indentator = this.copy(
+ depth = depth + 1,
+ indentation = indentation + unit
+ )
+
+ def backward: Indentator = this.copy(
+ depth = depth - 1,
+ indentation = if (indentation.nonEmpty) indentation.drop(1) else indentation
+ )
+}
+object Indentator {
+
+ def root(char: Char, size: Int): Indentator =
+ build(
+ char = char,
+ size = size,
+ depth = 0
+ )
+
+ def build(char: Char, size: Int, depth: Int): Indentator =
+ Indentator(
+ char = char,
+ size = size,
+ depth = depth,
+ indentation = (0 until size * depth).map(_ => char).mkString
+ )
+
+ def buildString(char: Char, size: Int, depth: Int): String =
+ (0 until size * depth).map(_ => char).mkString
+}
diff --git a/utils/src/main/scala/cats/xml/utils/generic/TypeInfo.scala b/utils/src/main/scala/cats/xml/utils/generic/TypeInfo.scala
new file mode 100644
index 0000000..0ab6125
--- /dev/null
+++ b/utils/src/main/scala/cats/xml/utils/generic/TypeInfo.scala
@@ -0,0 +1,38 @@
+package cats.xml.utils.generic
+
+import cats.Show
+
+case class TypeInfo[T] private (
+ isString: Boolean,
+ isPrimitiveWrapper: Boolean,
+ isPrimitive: Boolean,
+ isValueClass: Boolean,
+ accessorsInfo: Map[ParamName[T], TypeInfo[?]]
+) {
+ override def toString: String = Show[TypeInfo[T]].show(this)
+}
+object TypeInfo extends TypeInfoInstances {
+
+ def apply[T: TypeInfo]: TypeInfo[T] = implicitly[TypeInfo[T]]
+
+ def of[T](
+ isString: Boolean,
+ isPrimitiveWrapper: Boolean,
+ isPrimitive: Boolean,
+ isValueClass: Boolean,
+ accessorsInfo: Map[ParamName[T], TypeInfo[?]]
+ ): TypeInfo[T] = new TypeInfo[T](
+ isString,
+ isPrimitiveWrapper,
+ isPrimitive,
+ isValueClass,
+ accessorsInfo
+ )
+
+ implicit def showTypeInfo[T]: Show[TypeInfo[T]] =
+ (t: TypeInfo[T]) => s"""
+ |isString: ${t.isString}
+ |isPrimitive: ${t.isPrimitive}
+ |isValueClass: ${t.isValueClass}
+ |accessorsInfo: ${t.accessorsInfo}""".stripMargin
+}
diff --git a/modules/generic/src/main/scala/cats/xml/generic/stringOps.scala b/utils/src/main/scala/cats/xml/utils/stringOps.scala
similarity index 96%
rename from modules/generic/src/main/scala/cats/xml/generic/stringOps.scala
rename to utils/src/main/scala/cats/xml/utils/stringOps.scala
index bf8ef63..eb3a40d 100644
--- a/modules/generic/src/main/scala/cats/xml/generic/stringOps.scala
+++ b/utils/src/main/scala/cats/xml/utils/stringOps.scala
@@ -1,6 +1,6 @@
-package cats.xml.generic
+package cats.xml.utils
-import cats.xml.generic.StringMapper.*
+import cats.xml.utils.StringMapper.*
sealed trait StringMapper extends (String => String) {