aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVlad Uspensky <v.uspenskiy@icloud.com>2017-10-20 14:38:42 -0700
committerGitHub <noreply@github.com>2017-10-20 14:38:42 -0700
commit0cb06d70bd91e1e6a4ab9d97851ef9db7aaedfd6 (patch)
treedd0d66174b6294251570ce24e8bd7b864beb561a
parentfa6716e0a3223fa0b15966f6bdfe5464e26d8fc2 (diff)
parentba2c214d2f8f9a1ba6d8265c55476a5281b8fd22 (diff)
downloaddriver-core-0cb06d70bd91e1e6a4ab9d97851ef9db7aaedfd6.tar.gz
driver-core-0cb06d70bd91e1e6a4ab9d97851ef9db7aaedfd6.tar.bz2
driver-core-0cb06d70bd91e1e6a4ab9d97851ef9db7aaedfd6.zip
Merge pull request #76 from drivergroup/refinedv1.5.1
[RFC] Using "Refined" library (https://github.com/fthomas/refined) to…
-rw-r--r--build.sbt1
-rw-r--r--src/main/scala/xyz/driver/core/core.scala14
-rw-r--r--src/main/scala/xyz/driver/core/database/database.scala36
-rw-r--r--src/main/scala/xyz/driver/core/generators.scala17
-rw-r--r--src/main/scala/xyz/driver/core/json.scala39
-rw-r--r--src/test/scala/xyz/driver/core/GeneratorsTest.scala10
-rw-r--r--src/test/scala/xyz/driver/core/JsonTest.scala32
7 files changed, 144 insertions, 5 deletions
diff --git a/build.sbt b/build.sbt
index 2770f58..775da1b 100644
--- a/build.sbt
+++ b/build.sbt
@@ -22,5 +22,6 @@ lazy val core = (project in file("."))
"com.typesafe.slick" %% "slick" % "3.2.1",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.5.0",
+ "eu.timepit" %% "refined" % "0.8.4",
"ch.qos.logback" % "logback-classic" % "1.1.11"
))
diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala
index 4747574..c405962 100644
--- a/src/main/scala/xyz/driver/core/core.scala
+++ b/src/main/scala/xyz/driver/core/core.scala
@@ -1,6 +1,8 @@
package xyz.driver
import scalaz.{Equal, Monad, OptionT}
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection.NonEmpty
package object core {
@@ -83,6 +85,18 @@ package core {
implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(_.value)
}
+ final case class NonEmptyName[+Tag](value: String Refined NonEmpty) {
+ @inline def length: Int = value.value.length
+ override def toString: String = value.value
+ }
+
+ object NonEmptyName {
+ implicit def nonEmptyNameEqual[T]: Equal[NonEmptyName[T]] =
+ Equal.equal[NonEmptyName[T]](_.value.value == _.value.value)
+
+ implicit def nonEmptyNameOrdering[T]: Ordering[NonEmptyName[T]] = Ordering.by(_.value.value)
+ }
+
final case class Revision[T](id: String)
object Revision {
diff --git a/src/main/scala/xyz/driver/core/database/database.scala b/src/main/scala/xyz/driver/core/database/database.scala
index d305eee..26c1027 100644
--- a/src/main/scala/xyz/driver/core/database/database.scala
+++ b/src/main/scala/xyz/driver/core/database/database.scala
@@ -10,6 +10,11 @@ import com.typesafe.config.Config
package database {
+ import java.sql.SQLDataException
+
+ import eu.timepit.refined.api.{Refined, Validate}
+ import eu.timepit.refined.refineV
+
trait Database {
val profile: JdbcProfile
val database: JdbcProfile#Backend#Database
@@ -61,6 +66,33 @@ package database {
}
}
+ trait RefinedColumnTypes[T, Predicate] extends ColumnTypes {
+ import profile.api._
+ implicit def `eu.timepit.refined.api.Refined`(
+ implicit columnType: BaseColumnType[T],
+ validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate]
+ }
+
+ object RefinedColumnTypes {
+ trait RefinedValue[T, Predicate] extends RefinedColumnTypes[T, Predicate] {
+ import profile.api._
+ override implicit def `eu.timepit.refined.api.Refined`(
+ implicit columnType: BaseColumnType[T],
+ validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] =
+ MappedColumnType.base[T Refined Predicate, T](
+ _.value, { dbValue =>
+ refineV[Predicate](dbValue) match {
+ case Left(refinementError) =>
+ throw new SQLDataException(
+ s"Value in the database doesn't match the refinement constraints: $refinementError")
+ case Right(refinedValue) =>
+ refinedValue
+ }
+ }
+ )
+ }
+ }
+
trait IdColumnTypes extends ColumnTypes {
import profile.api._
implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]]
@@ -84,7 +116,7 @@ package database {
import profile.api._
override implicit def `xyz.driver.core.Id.columnType`[T] =
- MappedColumnType.base[Id[T], String](_.value, Id[T](_))
+ MappedColumnType.base[Id[T], String](_.value, Id[T])
}
}
@@ -117,7 +149,7 @@ package database {
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 naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T])
}
trait DatabaseObject extends ColumnTypes {
diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala
index 9242fd9..e6eb654 100644
--- a/src/main/scala/xyz/driver/core/generators.scala
+++ b/src/main/scala/xyz/driver/core/generators.scala
@@ -1,12 +1,16 @@
package xyz.driver.core
import java.math.MathContext
+import java.util.UUID
import xyz.driver.core.time.{Time, TimeRange}
import xyz.driver.core.date.Date
import scala.reflect.ClassTag
import scala.util.Random
+import eu.timepit.refined.refineV
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection._
object generators {
@@ -35,13 +39,22 @@ object generators {
def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength))
- def nextUuid() = java.util.UUID.randomUUID
+ def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] =
+ NonEmptyName[T](nextNonEmptyString(maxLength))
- def nextRevision[T]() = Revision[T](nextUuid().toString)
+ def nextUuid(): UUID = java.util.UUID.randomUUID
+
+ def nextRevision[T](): Revision[T] = Revision[T](nextUuid().toString)
def nextString(maxLength: Int = DefaultMaxLength): String =
(oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString
+ def nextNonEmptyString(maxLength: Int = DefaultMaxLength): String Refined NonEmpty = {
+ refineV[NonEmpty](
+ (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString
+ ).right.get
+ }
+
def nextOption[T](value: => T): Option[T] = if (nextBoolean()) Option(value) else None
def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right)
diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala
index 6e780ed..6b27a9c 100644
--- a/src/main/scala/xyz/driver/core/json.scala
+++ b/src/main/scala/xyz/driver/core/json.scala
@@ -14,6 +14,9 @@ import xyz.driver.core.auth.AuthCredentials
import xyz.driver.core.date.{Date, Month}
import xyz.driver.core.domain.{Email, PhoneNumber}
import xyz.driver.core.time.Time
+import eu.timepit.refined.refineV
+import eu.timepit.refined.api.{Refined, Validate}
+import eu.timepit.refined.collection.NonEmpty
object json {
import DefaultJsonProtocol._
@@ -213,6 +216,42 @@ object json {
}
}
+ /**
+ * Provides the JsonFormat for the Refined types provided by the Refined library.
+ *
+ * @see https://github.com/fthomas/refined
+ */
+ implicit def refinedJsonFormat[T, Predicate](implicit valueFormat: JsonFormat[T],
+ validate: Validate[T, Predicate]): JsonFormat[Refined[T, Predicate]] =
+ new JsonFormat[Refined[T, Predicate]] {
+ def write(x: T Refined Predicate): JsValue = valueFormat.write(x.value)
+ def read(value: JsValue): T Refined Predicate = {
+ refineV[Predicate](valueFormat.read(value))(validate) match {
+ case Right(refinedValue) => refinedValue
+ case Left(refinementError) => deserializationError(refinementError)
+ }
+ }
+ }
+
+ def NonEmptyNameInPath[T]: PathMatcher1[NonEmptyName[T]] = new PathMatcher1[NonEmptyName[T]] {
+ def apply(path: Path) = path match {
+ case Path.Segment(segment, tail) =>
+ refineV[NonEmpty](segment) match {
+ case Left(_) => Unmatched
+ case Right(nonEmptyString) => Matched(tail, Tuple1(NonEmptyName[T](nonEmptyString)))
+ }
+ case _ => Unmatched
+ }
+ }
+
+ implicit def nonEmptyNameFormat[T](implicit nonEmptyStringFormat: JsonFormat[Refined[String, NonEmpty]]) =
+ new RootJsonFormat[NonEmptyName[T]] {
+ def write(name: NonEmptyName[T]) = JsString(name.value.value)
+
+ def read(value: JsValue): NonEmptyName[T] =
+ NonEmptyName[T](nonEmptyStringFormat.read(value))
+ }
+
val jsValueToStringMarshaller: Marshaller[JsValue, String] =
Marshaller.strict[JsValue, String](value => Marshalling.Opaque[String](() => value.compactPrint))
diff --git a/src/test/scala/xyz/driver/core/GeneratorsTest.scala b/src/test/scala/xyz/driver/core/GeneratorsTest.scala
index 737cbcb..62ba7ae 100644
--- a/src/test/scala/xyz/driver/core/GeneratorsTest.scala
+++ b/src/test/scala/xyz/driver/core/GeneratorsTest.scala
@@ -44,6 +44,11 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions {
assert(!fixedLengthName.value.exists(_.isControl))
}
+ it should "be able to generate com.drivergrp.core.NonEmptyName with non empty strings" in {
+
+ assert(nextNonEmptyName[String]().value.value.nonEmpty)
+ }
+
it should "be able to generate proper UUIDs" in {
nextUuid() should not be nextUuid()
@@ -66,6 +71,11 @@ class GeneratorsTest extends FlatSpec with Matchers with Assertions {
assert(!fixedLengthString.exists(_.isControl))
}
+ it should "be able to generate strings non-empty strings whic are non empty" in {
+
+ assert(nextNonEmptyString().value.nonEmpty)
+ }
+
it should "be able to generate options which are sometimes have values and sometimes not" in {
val generatedOption = nextOption("2")
diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala
index bcf0ecf..26e3796 100644
--- a/src/test/scala/xyz/driver/core/JsonTest.scala
+++ b/src/test/scala/xyz/driver/core/JsonTest.scala
@@ -1,13 +1,17 @@
package xyz.driver.core
+import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.numeric.Positive
+import eu.timepit.refined.refineMV
import org.scalatest.{FlatSpec, Matchers}
-import xyz.driver.core.json.{EnumJsonFormat, GadtJsonFormat, ValueClassFormat}
+import xyz.driver.core.json._
import xyz.driver.core.time.provider.SystemTimeProvider
import spray.json._
import xyz.driver.core.TestTypes.CustomGADT
import xyz.driver.core.domain.{Email, PhoneNumber}
class JsonTest extends FlatSpec with Matchers {
+ import DefaultJsonProtocol._
"Json format for Id" should "read and write correct JSON" in {
@@ -31,6 +35,19 @@ class JsonTest extends FlatSpec with Matchers {
parsedName should be(referenceName)
}
+ "Json format for NonEmptyName" should "read and write correct JSON" in {
+
+ val jsonFormat = json.nonEmptyNameFormat[String]
+
+ val referenceNonEmptyName = NonEmptyName[String](refineMV[NonEmpty]("Homer"))
+
+ val writtenJson = jsonFormat.write(referenceNonEmptyName)
+ writtenJson.prettyPrint should be("\"Homer\"")
+
+ val parsedNonEmptyName = jsonFormat.read(writtenJson)
+ parsedNonEmptyName should be(referenceNonEmptyName)
+ }
+
"Json format for Time" should "read and write correct JSON" in {
val referenceTime = new SystemTimeProvider().currentTime()
@@ -168,4 +185,17 @@ class JsonTest extends FlatSpec with Matchers {
parsedValue1 should be(referenceValue1)
parsedValue2 should be(referenceValue2)
}
+
+ "Json format for a Refined value" should "read and write correct JSON" in {
+
+ val jsonFormat = json.refinedJsonFormat[Int, Positive]
+
+ val referenceRefinedNumber = refineMV[Positive](42)
+
+ val writtenJson = jsonFormat.write(referenceRefinedNumber)
+ writtenJson should be("42".parseJson)
+
+ val parsedRefinedNumber = jsonFormat.read(writtenJson)
+ parsedRefinedNumber should be(referenceRefinedNumber)
+ }
}