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) {