From bdf9ec57f213eb652ba5fb3b21973d028034d40e Mon Sep 17 00:00:00 2001 From: Sergey Nastich Date: Tue, 3 Apr 2018 10:57:14 -0700 Subject: Add enumeratum support to JSON format and generators (#144) * Add enumeratum support to JSON format and generators * Move enumeratum serializers into their own object. Add enumeratum unmarshaller. Add entities to derive JsonFormats instead of having to type them externally. --- src/main/scala/xyz/driver/core/date.scala | 17 ++++---- src/main/scala/xyz/driver/core/generators.scala | 3 ++ src/main/scala/xyz/driver/core/json.scala | 53 ++++++++++++++++++++----- 3 files changed, 55 insertions(+), 18 deletions(-) (limited to 'src/main/scala/xyz') diff --git a/src/main/scala/xyz/driver/core/date.scala b/src/main/scala/xyz/driver/core/date.scala index fe35c91..5454093 100644 --- a/src/main/scala/xyz/driver/core/date.scala +++ b/src/main/scala/xyz/driver/core/date.scala @@ -2,12 +2,13 @@ package xyz.driver.core import java.util.Calendar -import scala.util.Try - +import enumeratum._ import scalaz.std.anyVal._ -import scalaz.Scalaz.stringInstance import scalaz.syntax.equal._ +import scala.collection.immutable.IndexedSeq +import scala.util.Try + /** * Driver Date type and related validators/extractors. * Day, Month, and Year extractors are from ISO 8601 strings => driver...Date integers. @@ -15,8 +16,8 @@ import scalaz.syntax.equal._ */ object date { - sealed trait DayOfWeek - object DayOfWeek { + sealed trait DayOfWeek extends EnumEntry + object DayOfWeek extends Enum[DayOfWeek] { case object Monday extends DayOfWeek case object Tuesday extends DayOfWeek case object Wednesday extends DayOfWeek @@ -25,9 +26,11 @@ object date { case object Saturday extends DayOfWeek case object Sunday extends DayOfWeek - val All: Set[DayOfWeek] = Set(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday) + val values: IndexedSeq[DayOfWeek] = findValues + + val All: Set[DayOfWeek] = values.toSet - def fromString(day: String): Option[DayOfWeek] = All.find(_.toString === day) + def fromString(day: String): Option[DayOfWeek] = withNameInsensitiveOption(day) } type Day = Int @@ Day.type diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index 143044c..3c85447 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -1,5 +1,6 @@ package xyz.driver.core +import enumeratum._ import java.math.MathContext import java.util.UUID @@ -91,6 +92,8 @@ object generators { def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size)) + def oneOf[T <: EnumEntry](enum: Enum[T]): T = oneOf(enum.values: _*) + def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Array[T] = Array.fill(nextInt(maxLength, minLength))(generator) diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 4d7fa04..06a8837 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -3,21 +3,23 @@ package xyz.driver.core import java.net.InetAddress import java.util.{TimeZone, UUID} -import scala.reflect.runtime.universe._ -import scala.util.Try +import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} import akka.http.scaladsl.model.Uri.Path -import akka.http.scaladsl.server._ import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} -import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} +import akka.http.scaladsl.server._ import akka.http.scaladsl.unmarshalling.Unmarshaller +import enumeratum._ +import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.refineV import spray.json._ import xyz.driver.core.auth.AuthCredentials import xyz.driver.core.date.{Date, DayOfWeek, Month} import xyz.driver.core.domain.{Email, PhoneNumber} import xyz.driver.core.time.{Time, TimeOfDay} -import eu.timepit.refined.refineV -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty + +import scala.reflect.runtime.universe._ +import scala.util.Try object json { import DefaultJsonProtocol._ @@ -107,8 +109,7 @@ object json { implicit val timeOfDayFormat: RootJsonFormat[TimeOfDay] = jsonFormat2(TimeOfDay.apply) - implicit val dayOfWeekFormat: JsonFormat[DayOfWeek] = - new EnumJsonFormat[DayOfWeek](DayOfWeek.All.map(w => w.toString -> w)(collection.breakOut): _*) + implicit val dayOfWeekFormat: JsonFormat[DayOfWeek] = new enumeratum.EnumJsonFormat(DayOfWeek) implicit val dateFormat = new RootJsonFormat[Date] { def write(date: Date) = JsString(date.toString) @@ -136,9 +137,9 @@ object json { } implicit def revisionFromStringUnmarshaller[T]: Unmarshaller[String, Revision[T]] = - Unmarshaller.strict[String, Revision[T]](Revision[T](_)) + Unmarshaller.strict[String, Revision[T]](Revision[T]) - implicit def revisionFormat[T] = new RootJsonFormat[Revision[T]] { + implicit def revisionFormat[T]: RootJsonFormat[Revision[T]] = new RootJsonFormat[Revision[T]] { def write(revision: Revision[T]) = JsString(revision.id.toString) def read(value: JsValue): Revision[T] = value match { @@ -186,6 +187,36 @@ object json { JsString(obj.getHostAddress) } + object enumeratum { + + def enumUnmarshaller[T <: EnumEntry](enum: Enum[T]): Unmarshaller[String, T] = + Unmarshaller.strict { value => + enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) + } + + trait HasJsonFormat[T <: EnumEntry] { enum: Enum[T] => + + implicit val format: JsonFormat[T] = new EnumJsonFormat(enum) + + implicit val unmarshaller: Unmarshaller[String, T] = + Unmarshaller.strict { value => + enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) + } + } + + class EnumJsonFormat[T <: EnumEntry](enum: Enum[T]) extends JsonFormat[T] { + override def read(json: JsValue): T = json match { + case JsString(name) => enum.withNameOption(name).getOrElse(unrecognizedValue(name, enum.values)) + case _ => deserializationError("Expected string as enumeration value, but got " + json.toString) + } + + override def write(obj: T): JsValue = JsString(obj.entryName) + } + + private def unrecognizedValue(value: String, possibleValues: Seq[Any]): Nothing = + deserializationError(s"Unexpected value $value. Expected one of: ${possibleValues.mkString("[", ", ", "]")}") + } + class EnumJsonFormat[T](mapping: (String, T)*) extends RootJsonFormat[T] { private val map = mapping.toMap -- cgit v1.2.3