From de97eebf217f9e934decdb80bc840b9e1365a890 Mon Sep 17 00:00:00 2001 From: Kseniya Tomskikh Date: Mon, 8 Oct 2018 17:56:10 +0800 Subject: Created GenericId and typized id classes --- src/main/scala/xyz/driver/core/core.scala | 52 +++++++++++++++++++++- .../scala/xyz/driver/core/database/database.scala | 37 +++++++++++++++ src/main/scala/xyz/driver/core/generators.scala | 2 + src/main/scala/xyz/driver/core/json.scala | 18 ++++++++ .../driver/core/rest/directives/PathMatchers.scala | 6 +++ .../core/rest/directives/Unmarshallers.scala | 10 +++++ src/test/scala/xyz/driver/core/JsonTest.scala | 27 +++++++++++ 7 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index 2ab4e88..11c1ffe 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -64,7 +64,15 @@ package object core { package core { - final case class Id[+Tag](value: String) extends AnyVal { + import java.util.UUID + + sealed trait GenericId[+Tag, IdType] extends Any { + def value: IdType + def length: Int + def toString: String + } + + final case class Id[+Tag](value: String) extends AnyVal with GenericId[Tag, String] { @inline def length: Int = value.length override def toString: String = value } @@ -85,6 +93,48 @@ package core { implicit def convertER[E, R](id: Id[E])(implicit mapper: Mapper[E, R]): Id[R] = mapper[R](id) } + final case class UuidId[+Tag](value: UUID) extends AnyVal with GenericId[Tag, UUID] { + @inline def length: Int = value.toString.length + override def toString: String = value.toString + } + + @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) + object UuidId { + implicit def idEqual[T]: Equal[UuidId[T]] = Equal.equal[UuidId[T]](_ == _) + implicit def idOrdering[T]: Ordering[UuidId[T]] = Ordering.by[UuidId[T], UUID](_.value) + + sealed class Mapper[E, R] { + def apply[T >: E](id: UuidId[R]): UuidId[T] = UuidId[E](id.value) + def apply[T >: R](id: UuidId[E])(implicit dummy: DummyImplicit): UuidId[T] = UuidId[R](id.value) + } + object Mapper { + def apply[E, R] = new Mapper[E, R] + } + implicit def convertRE[R, E](id: UuidId[R])(implicit mapper: Mapper[E, R]): UuidId[E] = mapper[E](id) + implicit def convertER[E, R](id: UuidId[E])(implicit mapper: Mapper[E, R]): UuidId[R] = mapper[R](id) + } + + final case class NumericId[+Tag](value: Long) extends AnyVal with GenericId[Tag, Long] { + @inline def length: Int = value.toString.length + override def toString: String = value.toString + } + + @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) + object NumericId { + implicit def idEqual[T]: Equal[NumericId[T]] = Equal.equal[NumericId[T]](_ == _) + implicit def idOrdering[T]: Ordering[NumericId[T]] = Ordering.by[NumericId[T], Long](_.value) + + sealed class Mapper[E, R] { + def apply[T >: E](id: NumericId[R]): NumericId[T] = NumericId[E](id.value) + def apply[T >: R](id: NumericId[E])(implicit dummy: DummyImplicit): NumericId[T] = NumericId[R](id.value) + } + object Mapper { + def apply[E, R] = new Mapper[E, R] + } + implicit def convertRE[R, E](id: NumericId[R])(implicit mapper: Mapper[E, R]): NumericId[E] = mapper[E](id) + implicit def convertER[E, R](id: NumericId[E])(implicit mapper: Mapper[E, R]): NumericId[R] = mapper[R](id) + } + final case class Name[+Tag](value: String) extends AnyVal { @inline def length: Int = value.length override def toString: String = value diff --git a/src/main/scala/xyz/driver/core/database/database.scala b/src/main/scala/xyz/driver/core/database/database.scala index bd20b54..f3630ff 100644 --- a/src/main/scala/xyz/driver/core/database/database.scala +++ b/src/main/scala/xyz/driver/core/database/database.scala @@ -126,6 +126,33 @@ package database { } } + trait GenericIdColumnTypes[IdType] extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.GenericId.columnType`[T]: BaseColumnType[GenericId[T, IdType]] + } + + object GenericIdColumnTypes { + trait UUID extends GenericIdColumnTypes[java.util.UUID] { + import profile.api._ + + override implicit def `xyz.driver.core.GenericId.columnType`[T]: BaseColumnType[GenericId[T, java.util.UUID]] = + MappedColumnType + .base[GenericId[T, java.util.UUID], java.util.UUID](id => id.value, uuid => UuidId[T](uuid)) + } + trait SerialId extends GenericIdColumnTypes[Long] { + import profile.api._ + + override implicit def `xyz.driver.core.GenericId.columnType`[T]: BaseColumnType[GenericId[T, Long]] = + MappedColumnType.base[GenericId[T, Long], Long](_.value, serialId => NumericId[T](serialId)) + } + trait NaturalId extends GenericIdColumnTypes[String] { + import profile.api._ + + override implicit def `xyz.driver.core.GenericId.columnType`[T]: BaseColumnType[GenericId[T, String]] = + MappedColumnType.base[GenericId[T, String], String](_.value, Id[T]) + } + } + trait TimestampColumnTypes extends ColumnTypes { import profile.api._ implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] @@ -166,6 +193,16 @@ package database { def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) } + trait GenericKeyMappers extends ColumnTypes { + import profile.api._ + + def uuidKeyMapper[T] = + MappedColumnType + .base[UuidId[T], java.util.UUID](id => id.value, uuid => UuidId[T](uuid)) + def serialKeyMapper[T] = MappedColumnType.base[NumericId[T], Long](_.value, serialId => NumericId[T](serialId)) + def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) + } + trait DatabaseObject extends ColumnTypes { def createTables(): Future[Unit] def disconnect(): Unit diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index d00b6dd..0a4a7ab 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -65,6 +65,8 @@ object generators { def nextNumericId[T](maxValue: Int): Id[T] = Id[T](nextInt(maxValue).toString) + def nextUuidId[T](): UuidId[T] = UuidId[T](nextUuid()) + def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] = diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 4daf127..48011e4 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -37,6 +37,24 @@ object json extends PathMatchers with Unmarshallers { } } + implicit def uuidIdFormat[T]: RootJsonFormat[UuidId[T]] = new RootJsonFormat[UuidId[T]] { + def write(id: UuidId[T]) = JsString(id.toString) + + def read(value: JsValue): UuidId[T] = value match { + case JsString(id) if Try(UUID.fromString(id)).isSuccess => UuidId[T](UUID.fromString(id)) + case _ => throw DeserializationException("Id expects UUID") + } + } + + implicit def numericIdFormat[T]: RootJsonFormat[NumericId[T]] = new RootJsonFormat[NumericId[T]] { + def write(id: NumericId[T]) = JsString(id.toString) + + def read(value: JsValue): NumericId[T] = value match { + case JsString(id) if Try(id.toLong).isSuccess => NumericId[T](id.toLong) + case _ => throw DeserializationException("Id expects number") + } + } + implicit def taggedFormat[F, T](implicit underlying: JsonFormat[F], convert: F => F @@ T = null): JsonFormat[F @@ T] = new JsonFormat[F @@ T] { import tagging._ diff --git a/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala b/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala index 183ad9a..8ba184f 100644 --- a/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala +++ b/src/main/scala/xyz/driver/core/rest/directives/PathMatchers.scala @@ -27,6 +27,12 @@ trait PathMatchers { } } + def UuidIdInPath[T]: PathMatcher1[UuidId[T]] = + AkkaPathMatchers.JavaUUID.map((id: UUID) => UuidId[T](id)) + + def NumericIdInPath[T]: PathMatcher1[NumericId[T]] = + AkkaPathMatchers.LongNumber.map((id: Long) => NumericId[T](id)) + def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { def apply(path: Path) = path match { case Path.Segment(segment, tail) => Matched(tail, Tuple1(Name[T](segment))) diff --git a/src/main/scala/xyz/driver/core/rest/directives/Unmarshallers.scala b/src/main/scala/xyz/driver/core/rest/directives/Unmarshallers.scala index 6c45d15..93a9a52 100644 --- a/src/main/scala/xyz/driver/core/rest/directives/Unmarshallers.scala +++ b/src/main/scala/xyz/driver/core/rest/directives/Unmarshallers.scala @@ -16,6 +16,16 @@ trait Unmarshallers { Id[A](UUID.fromString(str).toString) } + implicit def uuidIdUnmarshaller[A]: Unmarshaller[String, UuidId[A]] = + Unmarshaller.strict[String, UuidId[A]] { str => + UuidId[A](UUID.fromString(str)) + } + + implicit def numericIdUnmarshaller[A]: Unmarshaller[Long, NumericId[A]] = + Unmarshaller.strict[Long, NumericId[A]] { x => + NumericId[A](x) + } + implicit def paramUnmarshaller[T](implicit reader: JsonReader[T]): Unmarshaller[String, T] = Unmarshaller.firstOf( Unmarshaller.strict((JsString(_: String)) andThen reader.read), diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala index 2aa3572..3e68d90 100644 --- a/src/test/scala/xyz/driver/core/JsonTest.scala +++ b/src/test/scala/xyz/driver/core/JsonTest.scala @@ -2,6 +2,7 @@ package xyz.driver.core import java.net.InetAddress import java.time.{Instant, LocalDate} +import java.util.UUID import akka.http.scaladsl.model.Uri import akka.http.scaladsl.server.PathMatcher @@ -41,6 +42,32 @@ class JsonTest extends WordSpec with Matchers with Inspectors { } } + "Json format for UuidId" should { + "read and write correct JSON" in { + + val referenceId = UuidId[String](UUID.fromString("c21c0ba6-05a2-4d4b-87ba-2405a5e83e64")) + + val writtenJson = json.uuidIdFormat.write(referenceId) + writtenJson.prettyPrint should be("\"c21c0ba6-05a2-4d4b-87ba-2405a5e83e64\"") + + val parsedId = json.uuidIdFormat.read(writtenJson) + parsedId should be(referenceId) + } + } + + "Json format for NumericId" should { + "read and write correct JSON" in { + + val referenceId = NumericId[String](1312) + + val writtenJson = json.numericIdFormat.write(referenceId) + writtenJson.prettyPrint should be("\"1312\"") + + val parsedId = json.numericIdFormat.read(writtenJson) + parsedId should be(referenceId) + } + } + "Json format for @@" should { "read and write correct JSON" in { trait Irrelevant -- cgit v1.2.3