diff --git a/README.md b/README.md index a2656061..8016d91d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Integrations are available for: - [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 +- [Json4s](http://json4s.org): JVM only ### Table of Contents @@ -750,6 +751,63 @@ ArgonautDevice.values.foreach { item => } ``` +## Json4s +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-json4s_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.beachape/enumeratum-json4s_2.11) + +### SBT + +To use enumeratum with [Json4s](http://json4s.org): + +```scala +libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum-json4s" % enumeratumVersion +) +``` + +### Usage + +#### Enum + +```scala +import enumeratum._ + +sealed trait TrafficLight extends EnumEntry +object TrafficLight extends Enum[TrafficLight] /* nothing extra here */ { + case object Red extends TrafficLight + case object Yellow extends TrafficLight + case object Green extends TrafficLight + + val values = findValues +} + +import org.json4s.DefaultFormats + +implicit val formats = DefaultFormats + Json4s.serializer(TrafficLight) + +``` + +#### ValueEnum + +```scala +import enumeratum.values._ + +sealed abstract class Device(val value: Short) extends ShortEnumEntry +case object Device + extends ShortEnum[Device] /* nothing extra here */ { + case object Phone extends Device(1) + case object Laptop extends Device(2) + case object Desktop extends Device(3) + case object Tablet extends Device(4) + + val values = findValues +} + +import org.json4s.DefaultFormats + +implicit val formats = DefaultFormats + Json4s.serializer(Device) + +``` + ## 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 60705fb8..01baecda 100644 --- a/build.sbt +++ b/build.sbt @@ -16,6 +16,7 @@ lazy val reactiveMongoVersion = "0.12.1" lazy val circeVersion = "0.7.0" lazy val uPickleVersion = "0.4.4" lazy val argonautVersion = "6.2-RC2" +lazy val json4sVersion = "3.5.0" def thePlayVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor >= 11 => "2.5.12" @@ -35,7 +36,8 @@ lazy val integrationProjectRefs = Seq( enumeratumCirceJs, enumeratumCirceJvm, enumeratumReactiveMongoBson, - enumeratumArgonaut + enumeratumArgonaut, + enumeratumJson4s ).map(Project.projectToRef) lazy val root = @@ -76,6 +78,7 @@ lazy val scala_2_12 = Project(id = "scala_2_12", enumeratumUPickleJs, enumeratumUPickleJvm, enumeratumArgonaut, + enumeratumJson4s, enumeratumReactiveMongoBson ).map(Project.projectToRef): _*) // base plus known 2.12 friendly libs @@ -275,6 +278,21 @@ lazy val enumeratumArgonaut = ) ) +lazy val enumeratumJson4s = + Project(id = "enumeratum-json4s", + base = file("enumeratum-json4s"), + settings = commonWithPublishSettings) + .settings(testSettings: _*) + .settings( + version := "1.5.9-SNAPSHOT", + crossScalaVersions := scalaVersionsAll, + libraryDependencies ++= Seq( + "org.json4s" %% "json4s-core" % json4sVersion, + "org.json4s" %% "json4s-native" % json4sVersion % "test", + "com.beachape" %% "enumeratum" % Versions.Core.stable + ) + ) + lazy val commonSettings = Seq( organization := "com.beachape", incOptions := incOptions.value.withLogRecompileOnMacro(false), diff --git a/enumeratum-json4s/src/main/scala/enumeratum/Json4s.scala b/enumeratum-json4s/src/main/scala/enumeratum/Json4s.scala new file mode 100644 index 00000000..83a496ab --- /dev/null +++ b/enumeratum-json4s/src/main/scala/enumeratum/Json4s.scala @@ -0,0 +1,18 @@ +package enumeratum + +import org.json4s.CustomSerializer +import org.json4s.JsonAST.JString + +@SuppressWarnings(Array("org.wartremover.warts.Any")) +object Json4s { + + def serializer[A <: EnumEntry: Manifest](enum: Enum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JString(s) if enum.withNameOption(s).isDefined => enum.withName(s) + }, + { + case x: A => JString(x.entryName) + } + )) + +} diff --git a/enumeratum-json4s/src/main/scala/enumeratum/values/Json4s.scala b/enumeratum-json4s/src/main/scala/enumeratum/values/Json4s.scala new file mode 100644 index 00000000..7f94257d --- /dev/null +++ b/enumeratum-json4s/src/main/scala/enumeratum/values/Json4s.scala @@ -0,0 +1,67 @@ +package enumeratum.values + +import org.json4s.CustomSerializer +import org.json4s.JsonAST.{JInt, JLong, JString} + +@SuppressWarnings(Array("org.wartremover.warts.Any")) +object Json4s { + + def serializer[A <: IntEnumEntry: Manifest](enum: IntEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JInt(i) if i <= Int.MaxValue && enum.withValueOpt(i.toInt).isDefined => enum.withValue(i.toInt) + case JLong(i) if i <= Int.MaxValue && enum.withValueOpt(i.toInt).isDefined => enum.withValue(i.toInt) + }, + { + case x: A => JLong(x.value.toLong) + } + )) + + def serializer[A <: LongEnumEntry: Manifest](enum: LongEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JInt(i) if enum.withValueOpt(i.toLong).isDefined => enum.withValue(i.toLong) + case JLong(i) if enum.withValueOpt(i).isDefined => enum.withValue(i) + }, + { + case x: A => JLong(x.value) + } + )) + + def serializer[A <: ShortEnumEntry: Manifest](enum: ShortEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JInt(i) if i <= Short.MaxValue.toInt && enum.withValueOpt(i.toShort).isDefined => enum.withValue(i.toShort) + case JLong(i) if i <= Short.MaxValue && enum.withValueOpt(i.toShort).isDefined => enum.withValue(i.toShort) + }, + { + case x: A => JLong(x.value.toLong) + } + )) + + def serializer[A <: StringEnumEntry: Manifest](enum: StringEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JString(s) if enum.withValueOpt(s).isDefined => enum.withValue(s) + }, + { + case x: A => JString(x.value) + } + )) + + def serializer[A <: ByteEnumEntry: Manifest](enum: ByteEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JInt(i) if i <= Byte.MaxValue.toInt && enum.withValueOpt(i.toByte).isDefined => enum.withValue(i.toByte) + case JLong(i) if i <= Byte.MaxValue && enum.withValueOpt(i.toByte).isDefined => enum.withValue(i.toByte) + }, + { + case x: A => JLong(x.value.toLong) + } + )) + + def serializer[A <: CharEnumEntry: Manifest](enum: CharEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => ( + { + case JString(s) if s.length == 1 && enum.withValueOpt(s.head).isDefined => enum.withValue(s.head) + }, + { + case x: A => JString(x.value.toString) + } + )) + +} diff --git a/enumeratum-json4s/src/test/scala/enumeratum/Json4sSpec.scala b/enumeratum-json4s/src/test/scala/enumeratum/Json4sSpec.scala new file mode 100644 index 00000000..f158b704 --- /dev/null +++ b/enumeratum-json4s/src/test/scala/enumeratum/Json4sSpec.scala @@ -0,0 +1,65 @@ +package enumeratum + +import org.json4s.{DefaultFormats, MappingException} +import org.json4s.native.Serialization +import org.scalatest.{FunSpec, Matchers} + +class Json4sSpec extends FunSpec with Matchers { + + implicit val formats = DefaultFormats + Json4s.serializer(TrafficLight) + + case class Data(tr: TrafficLight) + case class DataOpt(tr: Option[TrafficLight]) + + describe("to JSON") { + it("should serialize plain value to entryName") { + TrafficLight.values.foreach { value => + Serialization.write(Data(tr = value)) shouldBe ("""{"tr":"""" + value.entryName + """"}""") + } + } + + it("should serialize Some(value) to entryName") { + TrafficLight.values.foreach { value => + Serialization.write(DataOpt(tr = Some(value))) shouldBe ("""{"tr":"""" + value.entryName + """"}""") + } + } + + it("should serialize None to nothing") { + Serialization.write(DataOpt(tr = None)) shouldBe """{}""" + } + } + + describe("from JSON") { + it("should parse enum members when given proper encoding") { + TrafficLight.values.foreach { value => + Serialization.read[Data]("""{"tr":"""" + value.entryName + """"}""").tr shouldBe value + } + } + + it("should parse enum members into optional values") { + TrafficLight.values.foreach { value => + Serialization.read[DataOpt]("""{"tr":"""" + value.entryName + """"}""").tr shouldBe Some(value) + } + } + + it("should parse missing value into None") { + Serialization.read[DataOpt]("""{}""").tr shouldBe None + } + + it("should parse invalid value into None") { + Serialization.read[DataOpt]("""{"tr":"bogus"}""").tr shouldBe None + Serialization.read[DataOpt]("""{"tr":17}""").tr shouldBe None + Serialization.read[DataOpt]("""{"tr":true}""").tr shouldBe None + Serialization.read[DataOpt]("""{"tr":null}""").tr shouldBe None + } + + it("should fail to parse random JSON values to members") { + a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":"bogus"}""")) + a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":17}""")) + a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":true}""")) + a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":null}""")) + a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{}""")) + } + } + +} diff --git a/enumeratum-json4s/src/test/scala/enumeratum/TrafficLight.scala b/enumeratum-json4s/src/test/scala/enumeratum/TrafficLight.scala new file mode 100644 index 00000000..7ab9b577 --- /dev/null +++ b/enumeratum-json4s/src/test/scala/enumeratum/TrafficLight.scala @@ -0,0 +1,10 @@ +package enumeratum + +sealed trait TrafficLight extends EnumEntry +object TrafficLight extends Enum[TrafficLight] { + case object Red extends TrafficLight + case object Yellow extends TrafficLight + case object Green extends TrafficLight + + val values = findValues +} diff --git a/enumeratum-json4s/src/test/scala/enumeratum/values/Json4sValueEnumSpec.scala b/enumeratum-json4s/src/test/scala/enumeratum/values/Json4sValueEnumSpec.scala new file mode 100644 index 00000000..a98cbea5 --- /dev/null +++ b/enumeratum-json4s/src/test/scala/enumeratum/values/Json4sValueEnumSpec.scala @@ -0,0 +1,135 @@ +package enumeratum.values + +import org.json4s.{DefaultFormats, JObject, MappingException} +import org.json4s.JsonDSL._ +import org.json4s.native.Serialization +import org.scalatest.{FunSpec, Matchers} + +class Json4sValueEnumSpec extends FunSpec with Matchers { + + implicit val formats = DefaultFormats + + Json4s.serializer(Json4sMediaType) + Json4s.serializer(Json4sJsonLibs) + + Json4s.serializer(Json4sDevice) + Json4s.serializer(Json4sHttpMethod) + + Json4s.serializer(Json4sBool) + Json4s.serializer(Json4sDigits) + + val data = Data(Json4sMediaType.`application/jpeg`, Json4sJsonLibs.Json4s, Json4sDevice.Laptop, + Json4sHttpMethod.Put, Json4sBool.Maybe, Json4sDigits.Dos) + + describe("to JSON") { + it("should serialize plain value") { + Serialization.read[JObject](Serialization.write(data)) shouldBe + ("mediaType" -> 3) ~ ("jsonLib" -> 2) ~ ("device" -> 2) ~ ("httpMethod" -> "PUT") ~ ("bool" -> "?") ~ ("digits" -> 2) + } + + it("should serialize Some(value)") { + Serialization.write(DataOpt(Some(Json4sMediaType.`application/jpeg`))) shouldBe """{"mediaType":3}""" + } + + it("should serialize None to nothing") { + Serialization.write(DataOpt(None)) shouldBe """{}""" + } + + } + + describe("from JSON") { + it("should parse enum members when given proper encoding") { + Serialization.read[Data]("""{"mediaType":3,"jsonLib":2,"device":2,"httpMethod":"PUT","bool":"?","digits":2}""") shouldBe data + } + + it("should parse enum members into optional values") { + Serialization.read[DataOpt]("""{"mediaType":3}""") shouldBe DataOpt(Some(Json4sMediaType.`application/jpeg`)) + } + + it("should parse missing value into None") { + Serialization.read[DataOpt]("""{}""") shouldBe DataOpt(None) + } + + it("should parse invalid value into None") { + Serialization.read[DataOpt]("""{"mediaType":"bogus"}""") shouldBe DataOpt(None) + Serialization.read[DataOpt]("""{"mediaType":17}""") shouldBe DataOpt(None) + Serialization.read[DataOpt]("""{"mediaType":true}""") shouldBe DataOpt(None) + Serialization.read[DataOpt]("""{"mediaType":null}""") shouldBe DataOpt(None) + } + + it("should fail to parse random JSON values to members") { + a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":"bogus"}""")) + a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":17}""")) + a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":true}""")) + a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":null}""")) + a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{}""")) + } + } +} + +case class Data(mediaType: Json4sMediaType, + jsonLib: Json4sJsonLibs, + device: Json4sDevice, + httpMethod: Json4sHttpMethod, + bool: Json4sBool, + digits: Json4sDigits) + +case class DataOpt(mediaType: Option[Json4sMediaType]) + +case class DataSingle(mediaType: Json4sMediaType) + +sealed abstract class Json4sMediaType(val value: Long, name: String) extends LongEnumEntry +case object Json4sMediaType + extends LongEnum[Json4sMediaType] { + case object `text/json` extends Json4sMediaType(1L, "text/json") + case object `text/html` extends Json4sMediaType(2L, "text/html") + case object `application/jpeg` extends Json4sMediaType(3L, "application/jpeg") + + val values = findValues +} + +sealed abstract class Json4sJsonLibs(val value: Int) extends IntEnumEntry +case object Json4sJsonLibs + extends IntEnum[Json4sJsonLibs] { + case object Argonaut extends Json4sJsonLibs(1) + case object Json4s extends Json4sJsonLibs(2) + case object Circe extends Json4sJsonLibs(3) + case object PlayJson extends Json4sJsonLibs(4) + case object SprayJson extends Json4sJsonLibs(5) + case object UPickle extends Json4sJsonLibs(6) + + val values = findValues +} + +sealed abstract class Json4sDevice(val value: Short) extends ShortEnumEntry +case object Json4sDevice + extends ShortEnum[Json4sDevice] { + case object Phone extends Json4sDevice(1) + case object Laptop extends Json4sDevice(2) + case object Desktop extends Json4sDevice(3) + case object Tablet extends Json4sDevice(4) + + val values = findValues +} + +sealed abstract class Json4sHttpMethod(val value: String) extends StringEnumEntry +case object Json4sHttpMethod + extends StringEnum[Json4sHttpMethod] { + case object Get extends Json4sHttpMethod("GET") + case object Put extends Json4sHttpMethod("PUT") + case object Post extends Json4sHttpMethod("POST") + + val values = findValues +} + +sealed abstract class Json4sBool(val value: Char) extends CharEnumEntry +case object Json4sBool extends CharEnum[Json4sBool] { + case object True extends Json4sBool('T') + case object False extends Json4sBool('F') + case object Maybe extends Json4sBool('?') + + val values = findValues +} + +sealed abstract class Json4sDigits(val value: Byte) extends ByteEnumEntry +case object Json4sDigits extends ByteEnum[Json4sDigits] { + case object Uno extends Json4sDigits(1) + case object Dos extends Json4sDigits(2) + case object Tres extends Json4sDigits(3) + + val values = findValues +} diff --git a/publish-integration-libs b/publish-integration-libs index 7a982708..edd1f63d 100755 --- a/publish-integration-libs +++ b/publish-integration-libs @@ -13,4 +13,6 @@ say "Done 4" && sbt "project enumeratum-play" +clean +publish-signed && say "Done 5" && sbt "project enumeratum-argonaut" +clean +publish-signed && +say "Done 6" && +sbt "project enumeratum-json4s" +clean +publish-signed && say "All done" \ No newline at end of file