diff --git a/README.md b/README.md index 48320353..9b41a976 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Integrations are available for: - [Circe](https://github.com/travisbrown/circe): JVM and ScalaJS - [UPickle](http://www.lihaoyi.com/upickle-pprint/upickle/): JVM and ScalaJS - [ReactiveMongo BSON](http://reactivemongo.org/releases/0.11/documentation/bson/overview.html): JVM only +- [Argonaut](http://www.argonaut.io): JVM only ### Table of Contents @@ -46,8 +47,9 @@ Integrations are available for: 5. [Circe integration](#circe) 6. [UPickle integration](#upickle) 7. [ReactiveMongo BSON integration](#reactivemongo-bson) -8. [Slick integration](#slick-integration) -9. [Benchmarking](#benchmarking) +8. [Argonaut integration](#argonaut) +9. [Slick integration](#slick-integration) +10. [Benchmarking](#benchmarking) ## Quick start @@ -661,7 +663,69 @@ val reader = implicitly[BSONReader[BSONValue, BsonDrinks]] assert(reader.read(BSONInteger(3)) == BsonDrinks.Cola) ``` -### Slick integration +## Argonaut + +### SBT + +To use enumeratum with [Argonaut](http://www.argonaut.io): + +```scala +libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum" % enumeratumVersion, + "com.beachape" %% "enumeratum-argonaut" % enumeratumVersion +) +``` + +### Usage + +#### Enum + +```scala +import enumeratum._ + +sealed trait TrafficLight extends EnumEntry +object TrafficLight extends Enum[TrafficLight] with ArgonautEnum[TrafficLight] { + case object Red extends TrafficLight + case object Yellow extends TrafficLight + case object Green extends TrafficLight + val values = findValues +} + +import argonaut._ +import Argonaut._ + +TrafficLight.values.foreach { entry => + assert(entry.asJson == entry.entryName.asJson) +} + +``` + +#### ValueEnum + +```scala +import enumeratum.values._ + +sealed abstract class ArgonautDevice(val value: Short) extends ShortEnumEntry +case object ArgonautDevice + extends ShortEnum[ArgonautDevice] + with ShortArgonautEnum[ArgonautDevice] { + case object Phone extends ArgonautDevice(1) + case object Laptop extends ArgonautDevice(2) + case object Desktop extends ArgonautDevice(3) + case object Tablet extends ArgonautDevice(4) + + val values = findValues +} + +import argonaut._ +import Argonaut._ + +ArgonautDevice.values.foreach { item => + assert(item.asJson == item.value.asJson) +} +``` + +## Slick integration [Slick](http://slick.lightbend.com) doesn't have a separate integration at the moment. You just have to provide a `MappedColumnType` for each database column that should be represented as an enum on the Scala side. diff --git a/build.sbt b/build.sbt index 8d45eb4b..00c0a7f1 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,8 @@ lazy val root = enumeratumUPickleJvm, enumeratumCirceJs, enumeratumCirceJvm, - enumeratumReactiveMongoBson) + enumeratumReactiveMongoBson, + enumeratumArgonaut) lazy val core = crossProject .crossType(CrossType.Pure) @@ -173,6 +174,18 @@ lazy val enumeratumCirce = crossProject lazy val enumeratumCirceJs = enumeratumCirce.js lazy val enumeratumCirceJvm = enumeratumCirce.jvm +lazy val enumeratumArgonaut = + Project(id = "enumeratum-argonaut", + base = file("enumeratum-argonaut"), + settings = commonWithPublishSettings) + .settings( + libraryDependencies ++= Seq( + "io.argonaut" %% "argonaut" % "6.1" + ) + ) + .settings(testSettings: _*) + .dependsOn(coreJvm % "test->test;compile->compile") + lazy val commonSettings = Seq( organization := "com.beachape", version := theVersion, diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/ArgonautEnum.scala b/enumeratum-argonaut/src/main/scala/enumeratum/ArgonautEnum.scala new file mode 100644 index 00000000..8cc544ca --- /dev/null +++ b/enumeratum-argonaut/src/main/scala/enumeratum/ArgonautEnum.scala @@ -0,0 +1,14 @@ +package enumeratum + +import argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +trait ArgonautEnum[A <: EnumEntry] { this: Enum[A] => + + implicit val argonautEncoder: EncodeJson[A] = Argonauter.encoder(this) + + implicit val argonautDecoder: DecodeJson[A] = Argonauter.decoder(this) + +} diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/Argonauter.scala b/enumeratum-argonaut/src/main/scala/enumeratum/Argonauter.scala new file mode 100644 index 00000000..8b4bbe80 --- /dev/null +++ b/enumeratum-argonaut/src/main/scala/enumeratum/Argonauter.scala @@ -0,0 +1,45 @@ +package enumeratum + +import argonaut._ +import Argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +object Argonauter { + + private def encoder0[A <: EnumEntry](f: A => String): EncodeJson[A] = + stringEncoder.contramap(f) + + def encoder[A <: EnumEntry](enum: Enum[A]): EncodeJson[A] = + encoder0[A](_.entryName) + + def encoderLowercase[A <: EnumEntry](enum: Enum[A]): EncodeJson[A] = + encoder0[A](_.entryName.toLowerCase) + + def encoderUppercase[A <: EnumEntry](enum: Enum[A]): EncodeJson[A] = + encoder0[A](_.entryName.toUpperCase) + + private def decoder0[A <: EnumEntry](enum: Enum[A])(f: String => Option[A]): DecodeJson[A] = + DecodeJson { cursor => + stringDecoder(cursor).flatMap { enumStr => + f(enumStr) match { + case Some(a) => okResult(a) + case _ => failResult(s"$enumStr' is not a member of enum $enum", cursor.history) + } + } + } + + def decoder[A <: EnumEntry](enum: Enum[A]): DecodeJson[A] = + decoder0(enum)(enum.withNameOption) + + def decoderLowercaseOnly[A <: EnumEntry](enum: Enum[A]): DecodeJson[A] = + decoder0(enum)(enum.withNameLowercaseOnlyOption) + + def decoderUppercaseOnly[A <: EnumEntry](enum: Enum[A]): DecodeJson[A] = + decoder0(enum)(enum.withNameUppercaseOnlyOption) + + private val stringEncoder = implicitly[EncodeJson[String]] + private val stringDecoder = implicitly[DecodeJson[String]] + +} diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala b/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala new file mode 100644 index 00000000..2c3c4f78 --- /dev/null +++ b/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala @@ -0,0 +1,75 @@ +package enumeratum.values + +import argonaut._ +import Argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +sealed trait ArgonautValueEnum[ValueType, EntryType <: ValueEnumEntry[ValueType]] { + this: ValueEnum[ValueType, EntryType] => + + implicit def argonautEncoder: EncodeJson[EntryType] + implicit def argonautDecoder: DecodeJson[EntryType] + +} + +/** + * ArgonautEnum for IntEnumEntry + */ +trait IntArgonautEnum[EntryType <: IntEnumEntry] extends ArgonautValueEnum[Int, EntryType] { + this: ValueEnum[Int, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} + +/** + * ArgonautEnum for LongEnumEntry + */ +trait LongArgonautEnum[EntryType <: LongEnumEntry] extends ArgonautValueEnum[Long, EntryType] { + this: ValueEnum[Long, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} + +/** + * ArgonautEnum for ShortEnumEntry + */ +trait ShortArgonautEnum[EntryType <: ShortEnumEntry] extends ArgonautValueEnum[Short, EntryType] { + this: ValueEnum[Short, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} + +/** + * ArgonautEnum for StringEnumEntry + */ +trait StringArgonautEnum[EntryType <: StringEnumEntry] + extends ArgonautValueEnum[String, EntryType] { this: ValueEnum[String, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} + +/** + * ArgonautEnum for CharEnumEntry + */ +trait CharArgonautEnum[EntryType <: CharEnumEntry] extends ArgonautValueEnum[Char, EntryType] { + this: ValueEnum[Char, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} + +/** + * ArgonautEnum for ByteEnumEntry + */ +trait ByteArgonautEnum[EntryType <: ByteEnumEntry] extends ArgonautValueEnum[Byte, EntryType] { + this: ValueEnum[Byte, EntryType] => + + implicit val argonautEncoder: EncodeJson[EntryType] = Argonauter.encoder(this) + implicit val argonautDecoder: DecodeJson[EntryType] = Argonauter.decoder(this) +} diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/values/Argonauter.scala b/enumeratum-argonaut/src/main/scala/enumeratum/values/Argonauter.scala new file mode 100644 index 00000000..2720d1b5 --- /dev/null +++ b/enumeratum-argonaut/src/main/scala/enumeratum/values/Argonauter.scala @@ -0,0 +1,32 @@ +package enumeratum.values + +import argonaut._ +import Argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +object Argonauter { + + def encoder[ValueType: EncodeJson, EntryType <: ValueEnumEntry[ValueType]]( + enum: ValueEnum[ValueType, EntryType]): EncodeJson[EntryType] = { + val encodeValue = implicitly[EncodeJson[ValueType]] + EncodeJson { entry => + encodeValue(entry.value) + } + } + + def decoder[ValueType: DecodeJson, EntryType <: ValueEnumEntry[ValueType]]( + enum: ValueEnum[ValueType, EntryType]): DecodeJson[EntryType] = { + val decodeValue = implicitly[DecodeJson[ValueType]] + DecodeJson { cursor => + decodeValue(cursor).flatMap { value => + enum.withValueOpt(value) match { + case Some(entry) => okResult(entry) + case _ => failResult(s"$value is not a member of enum $enum", cursor.history) + } + } + } + } + +} diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/values/package.scala b/enumeratum-argonaut/src/main/scala/enumeratum/values/package.scala new file mode 100644 index 00000000..c733d9e3 --- /dev/null +++ b/enumeratum-argonaut/src/main/scala/enumeratum/values/package.scala @@ -0,0 +1,19 @@ +package enumeratum + +import argonaut.Argonaut._ +import argonaut.{DecodeJson, EncodeJson} + +/** + * Created by alonsodomin on 15/10/2016. + */ +package object values { + + implicit val argonautByteEncoder: EncodeJson[Byte] = EncodeJson { byte => + jNumber(byte.toShort) + } + + implicit val argonautByteDecoder: DecodeJson[Byte] = DecodeJson { cursor => + cursor.as[Short].map(_.toByte) + } + +} diff --git a/enumeratum-argonaut/src/test/scala/enumeratum/ArgonautSpec.scala b/enumeratum-argonaut/src/test/scala/enumeratum/ArgonautSpec.scala new file mode 100644 index 00000000..32bed574 --- /dev/null +++ b/enumeratum-argonaut/src/test/scala/enumeratum/ArgonautSpec.scala @@ -0,0 +1,63 @@ +package enumeratum + +import org.scalatest.{FunSpec, Matchers} + +import argonaut._ +import Argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +class ArgonautSpec extends FunSpec with Matchers { + + describe("to JSON") { + it("should work") { + TrafficLight.values.foreach { value => + value.asJson shouldBe value.entryName.asJson + } + } + + it("should work for lower case") { + TrafficLight.values.foreach { value => + value.asJson(Argonauter.encoderLowercase(TrafficLight)) shouldBe value.entryName.toLowerCase.asJson + } + } + + it("should work for upper case") { + TrafficLight.values.foreach { value => + value.asJson(Argonauter.encoderUppercase(TrafficLight)) shouldBe value.entryName.toUpperCase.asJson + } + } + } + + describe("from JSON") { + it("should parse enum members when given proper encoding") { + TrafficLight.values.foreach { value => + value.entryName.asJson.as[TrafficLight] shouldBe okResult(value) + } + } + + it("should parse enum members when given proper encoding for lower case") { + TrafficLight.values.foreach { value => + value.entryName.toLowerCase.asJson + .as[TrafficLight](Argonauter.decoderLowercaseOnly(TrafficLight)) shouldBe okResult(value) + } + } + + it("should parse enum members when given proper encoding for upper case") { + TrafficLight.values.foreach { value => + value.entryName.toUpperCase.asJson + .as[TrafficLight](Argonauter.decoderUppercaseOnly(TrafficLight)) shouldBe okResult(value) + } + } + + it("should fail to parse random JSON values to members") { + val results = Seq("XXL".asJson, Int.MaxValue.asJson).map(_.as[TrafficLight]) + results.foreach { res => + res.result.isLeft shouldBe true + res.history.map(_.toList) shouldBe Some(Nil) + } + } + } + +} diff --git a/enumeratum-argonaut/src/test/scala/enumeratum/TrafficLight.scala b/enumeratum-argonaut/src/test/scala/enumeratum/TrafficLight.scala new file mode 100644 index 00000000..0b3f4cd0 --- /dev/null +++ b/enumeratum-argonaut/src/test/scala/enumeratum/TrafficLight.scala @@ -0,0 +1,13 @@ +package enumeratum + +/** + * Created by alonsodomin on 14/10/2016. + */ +sealed trait TrafficLight extends EnumEntry +object TrafficLight extends Enum[TrafficLight] with ArgonautEnum[TrafficLight] { + case object Red extends TrafficLight + case object Yellow extends TrafficLight + case object Green extends TrafficLight + + val values = findValues +} diff --git a/enumeratum-argonaut/src/test/scala/enumeratum/values/ArgonautValueEnumSpec.scala b/enumeratum-argonaut/src/test/scala/enumeratum/values/ArgonautValueEnumSpec.scala new file mode 100644 index 00000000..6499daa0 --- /dev/null +++ b/enumeratum-argonaut/src/test/scala/enumeratum/values/ArgonautValueEnumSpec.scala @@ -0,0 +1,117 @@ +package enumeratum.values + +import org.scalatest.{FunSpec, Matchers} +import argonaut._ +import Argonaut._ + +/** + * Created by alonsodomin on 14/10/2016. + */ +class ArgonautValueEnumSpec extends FunSpec with Matchers { + + testArgonautEnum("LongArgonautEnum", ArgonautMediaType) + testArgonautEnum("IntArgonautEnum", ArgonautJsonLibs) + testArgonautEnum("ShortArgonautEnum", ArgonautDevice) + testArgonautEnum("CharArgonautEnum", ArgonautBool) + testArgonautEnum("StringArgonautEnum", ArgonautHttpMethod) + testArgonautEnum("ByteArgonautEnum", ArgonautDigits) + + private def testArgonautEnum[ValueType: EncodeJson: DecodeJson, + EntryType <: ValueEnumEntry[ValueType]: EncodeJson: DecodeJson]( + enumKind: String, + enum: ValueEnum[ValueType, EntryType] with ArgonautValueEnum[ValueType, EntryType]): Unit = { + describe(enumKind) { + + describe("from JSON") { + it("should work") { + enum.values.foreach { entry => + entry.asJson shouldBe entry.value.asJson + } + } + } + + describe("from JSON") { + it("should parse members when passing proper JSON values") { + enum.values.foreach { entry => + entry.asJson.as[EntryType] shouldBe okResult(entry) + } + } + + it("should fail to parse random JSON value") { + val results = Seq("NO".asJson, Long.MinValue.asJson).map(_.as[EntryType]) + results.foreach { res => + res.result.isLeft shouldBe true + res.history.map(_.toList) shouldBe Some(Nil) + } + } + } + } + } + +} + +sealed abstract class ArgonautMediaType(val value: Long, name: String) extends LongEnumEntry +case object ArgonautMediaType + extends LongEnum[ArgonautMediaType] + with LongArgonautEnum[ArgonautMediaType] { + case object `text/json` extends ArgonautMediaType(1L, "text/json") + case object `text/html` extends ArgonautMediaType(2L, "text/html") + case object `application/jpeg` extends ArgonautMediaType(3L, "application/jpeg") + + val values = findValues +} + +sealed abstract class ArgonautJsonLibs(val value: Int) extends IntEnumEntry +case object ArgonautJsonLibs + extends IntEnum[ArgonautJsonLibs] + with IntArgonautEnum[ArgonautJsonLibs] { + case object Json4s extends ArgonautJsonLibs(1) + case object Argonaut extends ArgonautJsonLibs(2) + case object Circe extends ArgonautJsonLibs(3) + case object PlayJson extends ArgonautJsonLibs(4) + case object SprayJson extends ArgonautJsonLibs(5) + case object UPickle extends ArgonautJsonLibs(6) + + val values = findValues +} + +sealed abstract class ArgonautDevice(val value: Short) extends ShortEnumEntry +case object ArgonautDevice + extends ShortEnum[ArgonautDevice] + with ShortArgonautEnum[ArgonautDevice] { + case object Phone extends ArgonautDevice(1) + case object Laptop extends ArgonautDevice(2) + case object Desktop extends ArgonautDevice(3) + case object Tablet extends ArgonautDevice(4) + + val values = findValues +} + +sealed abstract class ArgonautHttpMethod(val value: String) extends StringEnumEntry +case object ArgonautHttpMethod + extends StringEnum[ArgonautHttpMethod] + with StringArgonautEnum[ArgonautHttpMethod] { + case object Get extends ArgonautHttpMethod("GET") + case object Put extends ArgonautHttpMethod("PUT") + case object Post extends ArgonautHttpMethod("POST") + + val values = findValues +} + +sealed abstract class ArgonautBool(val value: Char) extends CharEnumEntry +case object ArgonautBool extends CharEnum[ArgonautBool] with CharArgonautEnum[ArgonautBool] { + case object True extends ArgonautBool('T') + case object False extends ArgonautBool('F') + case object Maybe extends ArgonautBool('?') + + val values = findValues +} + +sealed abstract class ArgonautDigits(val value: Byte) extends ByteEnumEntry +case object ArgonautDigits extends ByteEnum[ArgonautDigits] with ByteArgonautEnum[ArgonautDigits] { + case object Uno extends ArgonautDigits(1) + case object Dos extends ArgonautDigits(2) + case object Tres extends ArgonautDigits(3) + + val values = findValues +}