From f35323c7054a76d590974a76433defbd812bfd75 Mon Sep 17 00:00:00 2001 From: chrismcdonnell Date: Tue, 8 Oct 2024 10:00:09 -0400 Subject: [PATCH] Add Enumeratum ValueEnum codecs Also neeeded to bump the enumeratum version because of a bug in its ValueEnum macro for Scala 3 --- build.sbt | 6 +- .../phobos/enumeratum/XmlValueEnum.scala | 117 +++++++++++++++ .../enumeratum/EnumeratumValueTest.scala | 141 ++++++++++++++++++ 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 modules/enumeratum/src/main/scala/phobos/enumeratum/XmlValueEnum.scala create mode 100644 modules/enumeratum/src/test/scala/phobos/enumeratum/EnumeratumValueTest.scala diff --git a/build.sbt b/build.sbt index 365dca6..008d953 100644 --- a/build.sbt +++ b/build.sbt @@ -123,8 +123,12 @@ lazy val `enumeratum` = .settings(commonSettings("enumeratum")) .settings( commonDependencies, + scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => List( "-Yretain-trees") + case _ => Nil + }), libraryDependencies ++= Seq( - "com.beachape" %% "enumeratum" % "1.7.3", + "com.beachape" %% "enumeratum" % "1.7.5", ), ) .jvmPlatform(scala3Versions) diff --git a/modules/enumeratum/src/main/scala/phobos/enumeratum/XmlValueEnum.scala b/modules/enumeratum/src/main/scala/phobos/enumeratum/XmlValueEnum.scala new file mode 100644 index 0000000..10545fc --- /dev/null +++ b/modules/enumeratum/src/main/scala/phobos/enumeratum/XmlValueEnum.scala @@ -0,0 +1,117 @@ +package phobos.enumeratum + +import phobos.decoding._ +import phobos.encoding._ + +import enumeratum.values._ + +sealed trait XmlValueEnum[V, E <: ValueEnumEntry[V]] { + _enum: ValueEnum[V, E] => + + implicit def elementEncoder: ElementEncoder[E] + implicit def attributeEncoder: AttributeEncoder[E] + implicit def textEncoder: TextEncoder[E] + implicit def elementDecoder: ElementDecoder[E] + implicit def attributeDecoder: AttributeDecoder[E] + implicit def textDecoder: TextDecoder[E] + +} + +trait IntXmlValueEnum[E <: IntEnumEntry] extends XmlValueEnum[Int, E] { + self: IntEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[Int, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[Int, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[Int, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +trait LongXmlValueEnum[E <: LongEnumEntry] extends XmlValueEnum[Long, E] { + self: LongEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[Long, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[Long, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[Long, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +trait ShortXmlValueEnum[E <: ShortEnumEntry] extends XmlValueEnum[Short, E] { + self: ShortEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[Short, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[Short, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[Short, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +trait StringXmlValueEnum[E <: StringEnumEntry] extends XmlValueEnum[String, E] { + self: StringEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[String, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[String, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[String, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +trait CharXmlValueEnum[E <: CharEnumEntry] extends XmlValueEnum[Char, E] { + self: CharEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[Char, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[Char, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[Char, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +trait ByteXmlValueEnum[E <: ByteEnumEntry] extends XmlValueEnum[Byte, E] { + self: ByteEnum[E] => + implicit val elementEncoder: ElementEncoder[E] = XmlValueEnum.elementEncoder[Byte, E] + implicit val attributeEncoder: AttributeEncoder[E] = XmlValueEnum.attributeEncoder[Byte, E] + implicit val textEncoder: TextEncoder[E] = XmlValueEnum.textEncoder[Byte, E] + implicit val elementDecoder: ElementDecoder[E] = XmlValueEnum.elementDecoder(self) + implicit val attributeDecoder: AttributeDecoder[E] = XmlValueEnum.attributeDecoder(self) + implicit val textDecoder: TextDecoder[E] = XmlValueEnum.textDecoder(self) +} + +object XmlValueEnum { + + def elementDecoder[V, E <: ValueEnumEntry[V]]( + e: ValueEnum[V, E], + )(implicit baseDecoder: ElementDecoder[V]): ElementDecoder[E] = + baseDecoder.emap(decodeFromValueType(e)) + + def attributeDecoder[V, E <: ValueEnumEntry[V]]( + e: ValueEnum[V, E], + )(implicit baseDecoder: AttributeDecoder[V]): AttributeDecoder[E] = + baseDecoder.emap(decodeFromValueType(e)) + + def textDecoder[V, E <: ValueEnumEntry[V]]( + e: ValueEnum[V, E], + )(implicit baseDecoder: TextDecoder[V]): TextDecoder[E] = + baseDecoder.emap(decodeFromValueType(e)) + + def decodeFromValueType[V, E <: ValueEnumEntry[V]]( + e: ValueEnum[V, E], + )(history: List[String], value: V): Either[DecodingError, E] = + e.withValueOpt(value) match { + case Some(member) => Right(member) + case _ => Left(DecodingError(s"'$value' in not a member of enum $this", history, None)) + } + + def elementEncoder[V, E <: ValueEnumEntry[V]]( + implicit baseEncoder: ElementEncoder[V], + ): ElementEncoder[E] = baseEncoder.contramap(_.value) + + def attributeEncoder[V, E <: ValueEnumEntry[V]]( + implicit baseEncoder: AttributeEncoder[V], + ): AttributeEncoder[E] = baseEncoder.contramap(_.value) + + def textEncoder[V, E <: ValueEnumEntry[V]]( + implicit baseEncoder: TextEncoder[V], + ): TextEncoder[E] = baseEncoder.contramap(_.value) + +} diff --git a/modules/enumeratum/src/test/scala/phobos/enumeratum/EnumeratumValueTest.scala b/modules/enumeratum/src/test/scala/phobos/enumeratum/EnumeratumValueTest.scala new file mode 100644 index 0000000..7a34fe7 --- /dev/null +++ b/modules/enumeratum/src/test/scala/phobos/enumeratum/EnumeratumValueTest.scala @@ -0,0 +1,141 @@ +package phobos.enumeratum + +import phobos.decoding.XmlDecoder +import phobos.derivation.semiauto._ +import phobos.encoding.XmlEncoder +import phobos.syntax._ +import phobos.testString._ + +import enumeratum.values._ +import org.scalatest.Assertion +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +sealed abstract class Foo(override val value: Int) extends IntEnumEntry with Product with Serializable +object Foo extends IntEnum[Foo] with IntXmlValueEnum[Foo] { + val values = findValues + + case object Foo1 extends Foo(1) + case object Foo2 extends Foo(2) + case object Foo3 extends Foo(3) +} +import Foo._ +case class Bar(d: String, foo: Foo, e: Char) +object Bar { + implicit lazy val xmlEncoder: XmlEncoder[Bar] = deriveXmlEncoder[Bar]("bar") + implicit lazy val xmlDecoder: XmlDecoder[Bar] = deriveXmlDecoder[Bar]("bar") +} +case class Baz(@attr f: Foo, @text text: Foo) +object Baz { + implicit lazy val xmlEncoder: XmlEncoder[Baz] = deriveXmlEncoder[Baz]("baz") + implicit lazy val xmlDecoder: XmlDecoder[Baz] = deriveXmlDecoder[Baz]("baz") +} + +class EnumeratumValueTest extends AnyWordSpec with Matchers { + "Enum codecs" should { + "encode enums" in { + val bar1 = Bar("d value", Foo1, 'e') + val bar2 = Bar("d value", Foo2, 'e') + val bar3 = Bar("another one value", Foo3, 'v') + val baz = Baz(Foo1, Foo2) + val xml1 = XmlEncoder[Bar].encode(bar1) + val xml2 = XmlEncoder[Bar].encode(bar2) + val xml3 = XmlEncoder[Bar].encode(bar3) + val xml4 = XmlEncoder[Baz].encode(baz) + val string1 = + """ + | + | + | d value + | + | 1 + | + | e + | + """.stripMargin.minimized + val string2 = + """ + | + | + | d value + | + | 2 + | + | e + | + """.stripMargin.minimized + val string3 = + """ + | + | + | another one value + | + | 3 + | + | v + | + """.stripMargin.minimized + val string4 = + """ + | + | 2 + """.stripMargin.minimized + assert( + xml1 == Right(string1) && + xml2 == Right(string2) && + xml3 == Right(string3) && + xml4 == Right(string4), + ) + } + + def pure(str: String): List[Array[Byte]] = + List(str.getBytes("UTF-8")) + + def fromIterable(str: String): List[Array[Byte]] = + str.toList.map(c => Array(c.toByte)) + + def decodeEnums(toList: String => List[Array[Byte]]): Assertion = { + val bar1 = Bar("d value", Foo1, 'e') + val bar2 = Bar("d value", Foo2, 'e') + val bar3 = Bar("another one value", Foo3, 'v') + val baz = Baz(Foo1, Foo2) + + val string1 = + """ + | + | d value + | 1 + | e + | + """.stripMargin + val string2 = + """ + | + | d value + | 2 + | e + | + """.stripMargin + val string3 = + """ + | + | another one value + | 3 + | v + | + """.stripMargin + val string4 = + """ + | 2 + """.stripMargin + val decoded1 = XmlDecoder[Bar].decodeFromIterable(toList(string1)) + val decoded2 = XmlDecoder[Bar].decodeFromIterable(toList(string2)) + val decoded3 = XmlDecoder[Bar].decodeFromIterable(toList(string3)) + val decoded4 = XmlDecoder[Baz].decodeFromIterable(toList(string4)) + assert(decoded1 == Right(bar1) && decoded2 == Right(bar2) && decoded3 == Right(bar3) && decoded4 == Right(baz)) + } + + "decode enums sync" in decodeEnums(pure) + "decode enums async" in decodeEnums(fromIterable) + } +}