Skip to content

Commit

Permalink
Json4s integration, fixes #121 (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelstaldal authored and lloydmeta committed Mar 7, 2017
1 parent de3c252 commit 260ffe7
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 1 deletion.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
20 changes: 19 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -35,7 +36,8 @@ lazy val integrationProjectRefs = Seq(
enumeratumCirceJs,
enumeratumCirceJvm,
enumeratumReactiveMongoBson,
enumeratumArgonaut
enumeratumArgonaut,
enumeratumJson4s
).map(Project.projectToRef)

lazy val root =
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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),
Expand Down
18 changes: 18 additions & 0 deletions enumeratum-json4s/src/main/scala/enumeratum/Json4s.scala
Original file line number Diff line number Diff line change
@@ -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)
}
))

}
67 changes: 67 additions & 0 deletions enumeratum-json4s/src/main/scala/enumeratum/values/Json4s.scala
Original file line number Diff line number Diff line change
@@ -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)
}
))

}
65 changes: 65 additions & 0 deletions enumeratum-json4s/src/test/scala/enumeratum/Json4sSpec.scala
Original file line number Diff line number Diff line change
@@ -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]("""{}"""))
}
}

}
10 changes: 10 additions & 0 deletions enumeratum-json4s/src/test/scala/enumeratum/TrafficLight.scala
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 260ffe7

Please sign in to comment.