From 93b6eb324feacd2d52afcf8635a3d8e197f01f84 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 5 Mar 2018 19:57:27 -0800 Subject: Add StringId, LongId, and UuidId types to core --- src/main/scala/xyz/driver/core/core.scala | 31 +++++++++++++------- .../scala/xyz/driver/core/database/database.scala | 24 +++++++-------- src/main/scala/xyz/driver/core/json.scala | 34 +++++++++++++++++----- .../core/rest/auth/CachedTokenAuthorization.scala | 3 +- src/test/scala/xyz/driver/core/CoreTest.scala | 34 +++++++++++++--------- 5 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index be19f0f..9e6714d 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -54,25 +54,34 @@ package object core { package core { - final case class Id[+Tag](value: String) extends AnyVal { - @inline def length: Int = value.length - override def toString: String = value + sealed trait Id[+Tag] { + type Value + val value: Value + @inline def length: Int = toString.length + override def toString: String = value.toString } + final case class StringId[+Tag](value: String) extends Id[Tag] { type Value = String } + final case class LongId[+Tag](value: Long) extends Id[Tag] { type Value = Long } + final case class UuidId[+Tag](value: java.util.UUID) extends Id[Tag] { type Value = java.util.UUID } + @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) object Id { - implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _) - implicit def idOrdering[T]: Ordering[Id[T]] = Ordering.by[Id[T], String](_.value) + def apply[T](value: String): StringId[T] = StringId[T](value) + + implicit def idEqual[T, I[_] <: Id[_]]: Equal[I[T]] = Equal.equalA[I[T]] + implicit def idOrdering[T, I[_] <: Id[_]](implicit ord: Ordering[I[T]#Value]): Ordering[I[T]] = + Ordering.by[I[T], I[T]#Value](_.value) - sealed class Mapper[E, R] { - def apply[T >: E](id: Id[R]): Id[T] = Id[E](id.value) - def apply[T >: R](id: Id[E])(implicit dummy: DummyImplicit): Id[T] = Id[R](id.value) + sealed class Mapper[E, R, I[_] <: Id[_]] { + def apply[T >: E](id: I[R]): I[T] = id.asInstanceOf[I[T]] + def apply[T >: R](id: I[E])(implicit dummy: DummyImplicit): I[T] = id.asInstanceOf[I[T]] } object Mapper { - def apply[E, R] = new Mapper[E, R] + def apply[E, R, I[_] <: Id[_]] = new Mapper[E, R, I] } - implicit def convertRE[R, E](id: Id[R])(implicit mapper: Mapper[E, R]): Id[E] = mapper[E](id) - implicit def convertER[E, R](id: Id[E])(implicit mapper: Mapper[E, R]): Id[R] = mapper[R](id) + implicit def convertRE[R, E, I[_] <: Id[_]](id: I[R])(implicit mapper: Mapper[E, R, I]): I[E] = mapper[E](id) + implicit def convertER[E, R, I[_] <: Id[_]](id: I[E])(implicit mapper: Mapper[E, R, I]): I[R] = mapper[R](id) } final case class Name[+Tag](value: String) extends AnyVal { diff --git a/src/main/scala/xyz/driver/core/database/database.scala b/src/main/scala/xyz/driver/core/database/database.scala index ae06517..30adbf6 100644 --- a/src/main/scala/xyz/driver/core/database/database.scala +++ b/src/main/scala/xyz/driver/core/database/database.scala @@ -93,30 +93,30 @@ package database { } } - trait IdColumnTypes extends ColumnTypes { + trait IdColumnTypes[I[_] <: Id[_]] extends ColumnTypes { import profile.api._ - implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] + implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[I[T]] } object IdColumnTypes { - trait UUID extends IdColumnTypes { + trait UUID extends IdColumnTypes[UuidId] { import profile.api._ override implicit def `xyz.driver.core.Id.columnType`[T] = MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + .base[UuidId[T], java.util.UUID](_.value, UuidId[T]) } - trait SerialId extends IdColumnTypes { + trait SerialId extends IdColumnTypes[LongId] { import profile.api._ override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + MappedColumnType.base[LongId[T], Long](_.value, LongId[T]) } - trait NaturalId extends IdColumnTypes { + trait NaturalId extends IdColumnTypes[StringId] { import profile.api._ override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], String](_.value, Id[T]) + MappedColumnType.base[StringId[T], String](_.value, StringId[T]) } } @@ -146,11 +146,9 @@ package database { trait KeyMappers extends ColumnTypes { import profile.api._ - def uuidKeyMapper[T] = - MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) - def serialKeyMapper[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) + def uuidKeyMapper[T] = MappedColumnType.base[UuidId[T], java.util.UUID](_.value, UuidId[T]) + def serialKeyMapper[T] = MappedColumnType.base[LongId[T], Long](_.value, LongId[T]) + def naturalKeyMapper[T] = MappedColumnType.base[StringId[T], String](_.value, StringId[T]) } trait DatabaseObject extends ColumnTypes { diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 02a35fd..c18cadf 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -22,18 +22,38 @@ import eu.timepit.refined.collection.NonEmpty object json { import DefaultJsonProtocol._ - private def UuidInPath[T]: PathMatcher1[Id[T]] = - PathMatchers.JavaUUID.map((id: UUID) => Id[T](id.toString.toLowerCase)) + def UuidInPath[T]: PathMatcher1[UuidId[T]] = PathMatchers.JavaUUID.map(UuidId[T]) + def LongIdInPath[T]: PathMatcher1[LongId[T]] = PathMatchers.LongNumber.map(LongId[T]) + def StringIdInPath[T]: PathMatcher1[StringId[T]] = PathMatchers.Segment.map(StringId[T]) - def IdInPath[T]: PathMatcher1[Id[T]] = UuidInPath[T] | new PathMatcher1[Id[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => Matched(tail, Tuple1(Id[T](segment))) - case _ => Unmatched + def IdInPath[T]: PathMatcher1[Id[T]] = UuidInPath[T] | LongIdInPath[T] | StringIdInPath[T] + + implicit def stringIdFormat[T]: JsonFormat[StringId[T]] = new JsonFormat[StringId[T]] { + override def read(json: JsValue): StringId[T] = json match { + case JsString(s) => StringId[T](s) + case _ => deserializationError(s"Expected string for ID, got $json") + } + override def write(obj: StringId[T]): JsValue = JsString(obj.value) + } + + implicit def uuidIdFormat[T]: JsonFormat[UuidId[T]] = new JsonFormat[UuidId[T]] { + override def read(json: JsValue): UuidId[T] = json match { + case JsString(s) => UuidId[T](Try(UUID.fromString(s)).getOrElse(deserializationError(s"Invalid UUID format: $s"))) + case _ => deserializationError(s"Expected UUID string for ID, got $json") + } + override def write(obj: UuidId[T]): JsValue = JsString(obj.toString) + } + + implicit def longIdFormat[T]: JsonFormat[LongId[T]] = new JsonFormat[LongId[T]] { + override def read(json: JsValue): LongId[T] = json match { + case JsNumber(n) => LongId[T](n.toLong) + case _ => deserializationError(s"Expected number for ID, got $json") } + override def write(obj: LongId[T]): JsValue = JsNumber(obj.value) } implicit def idFormat[T] = new RootJsonFormat[Id[T]] { - def write(id: Id[T]) = JsString(id.value) + def write(id: Id[T]) = JsString(id.toString) def read(value: JsValue) = value match { case JsString(id) if Try(UUID.fromString(id)).isSuccess => Id[T](id.toLowerCase) diff --git a/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala index 66de4ef..38e52bc 100644 --- a/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala +++ b/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala @@ -7,6 +7,7 @@ import java.security.spec.X509EncodedKeySpec import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth.{Permission, User} import xyz.driver.core.rest.ServiceRequestContext +import xyz.driver.core.json.idFormat import scala.concurrent.Future import scalaz.syntax.std.boolean._ @@ -30,7 +31,7 @@ class CachedTokenAuthorization[U <: User](publicKey: => PublicKey, issuer: Strin jwtJson = jwt.parseJson.asJsObject // Ensure jwt is for the currently authenticated user and the correct issuer, otherwise return None - _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(()) + _ <- jwtJson.fields.get("sub").contains(user.id.toJson).option(()) _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(()) permissionsMap <- extractPermissionsFromTokenJSON(jwtJson) diff --git a/src/test/scala/xyz/driver/core/CoreTest.scala b/src/test/scala/xyz/driver/core/CoreTest.scala index d280d73..a138332 100644 --- a/src/test/scala/xyz/driver/core/CoreTest.scala +++ b/src/test/scala/xyz/driver/core/CoreTest.scala @@ -7,6 +7,9 @@ import org.scalatest.mockito.MockitoSugar import org.scalatest.{FlatSpec, Matchers} class CoreTest extends FlatSpec with Matchers with MockitoSugar { + // === is already in scope from org.scalactic.TripleEquals + def `====`[T: scalaz.Equal](a: T, b: T): Boolean = + implicitly[scalaz.Equal[T]].equal(a, b) "'make' function" should "allow initialization for objects" in { @@ -29,10 +32,10 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { } "Id" should "have equality and ordering working correctly" in { - - (Id[String]("1234213") === Id[String]("1234213")) should be(true) - (Id[String]("1234213") === Id[String]("213414")) should be(false) - (Id[String]("213414") === Id[String]("1234213")) should be(false) + ====(Id[String]("1234213"), Id[String]("1234213")) should be(true) + ====(Id[String]("1234213"), Id[String]("213414")) should be(false) + ====(Id[String]("213414"), Id[String]("1234213")) should be(false) + ====[Id[String]](StringId[String]("1"), LongId[String](1L)) should be(false) Seq(Id[String]("4"), Id[String]("3"), Id[String]("2"), Id[String]("1")).sorted should contain theSameElementsInOrderAs(Seq(Id[String]("1"), Id[String]("2"), Id[String]("3"), Id[String]("4"))) @@ -43,8 +46,11 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { final case class Y(id: Id[Y]) final case class Z(id: Id[Z]) - implicit val xy = Id.Mapper[X, Y] - implicit val yz = Id.Mapper[Y, Z] + implicit val equalX = scalaz.Equal.equalA[X] + implicit val equalY = scalaz.Equal.equalA[Y] + + implicit val xy = Id.Mapper[X, Y, Id] + implicit val yz = Id.Mapper[Y, Z, Id] // Test that implicit conversions work correctly val x = X(Id("0")) @@ -52,8 +58,8 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { val z = Z(y.id) val y2 = Y(z.id) val x2 = X(y2.id) - (x2 === x) should be(true) - (y2 === y) should be(true) + ====(x2, x) should be(true) + ====(y2, y) should be(true) // Test that type inferrence for explicit conversions work correctly val yid = y.id @@ -64,9 +70,9 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { "Name" should "have equality and ordering working correctly" in { - (Name[String]("foo") === Name[String]("foo")) should be(true) - (Name[String]("foo") === Name[String]("bar")) should be(false) - (Name[String]("bar") === Name[String]("foo")) should be(false) + ====(Name[String]("foo"), Name[String]("foo")) should be(true) + ====(Name[String]("foo"), Name[String]("bar")) should be(false) + ====(Name[String]("bar"), Name[String]("foo")) should be(false) Seq(Name[String]("d"), Name[String]("cc"), Name[String]("a"), Name[String]("bbb")).sorted should contain theSameElementsInOrderAs(Seq(Name[String]("a"), Name[String]("bbb"), Name[String]("cc"), Name[String]("d"))) @@ -77,8 +83,8 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar { val bla = Revision[String]("85569dab-a3dc-401b-9f95-d6fb4162674b") val foo = Revision[String]("f54b3558-bdcd-4646-a14b-8beb11f6b7c4") - (bla === bla) should be(true) - (bla === foo) should be(false) - (foo === bla) should be(false) + ====(bla, bla) should be(true) + ====(bla, foo) should be(false) + ====(foo, bla) should be(false) } } -- cgit v1.2.3