From c13a90f7dc6565e0beddcc6a61609d4e131f55ba Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 19 Oct 2017 14:42:50 -0700 Subject: [RFC] Using "Refined" library (https://github.com/fthomas/refined) to allow defining entities with more precise types --- build.sbt | 1 + src/main/scala/xyz/driver/core/core.scala | 14 ++++++++++++++ src/main/scala/xyz/driver/core/generators.scala | 14 ++++++++++++-- src/main/scala/xyz/driver/core/json.scala | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+), 2 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/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index 9242fd9..f3913e5 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,19 @@ object generators { def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) - def nextUuid() = java.util.UUID.randomUUID + def nextUuid(): UUID = java.util.UUID.randomUUID - def nextRevision[T]() = Revision[T](nextUuid().toString) + 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..c14424d 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -14,6 +14,8 @@ 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} object json { import DefaultJsonProtocol._ @@ -213,6 +215,23 @@ 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) + } + } + } + val jsValueToStringMarshaller: Marshaller[JsValue, String] = Marshaller.strict[JsValue, String](value => Marshalling.Opaque[String](() => value.compactPrint)) -- cgit v1.2.3 From fc2f37b4a3c22747d0e913a2b7a379dbe7e9e7d1 Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 20 Oct 2017 14:17:05 -0700 Subject: Slick support for any Refined types, JSON format and generator for NonEmptyName, Unit-tests --- .../scala/xyz/driver/core/database/database.scala | 36 ++++++++++++++++++++-- src/main/scala/xyz/driver/core/json.scala | 20 ++++++++++++ .../scala/xyz/driver/core/GeneratorsTest.scala | 10 ++++++ src/test/scala/xyz/driver/core/JsonTest.scala | 32 ++++++++++++++++++- 4 files changed, 95 insertions(+), 3 deletions(-) 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/json.scala b/src/main/scala/xyz/driver/core/json.scala index c14424d..6b27a9c 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -16,6 +16,7 @@ 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._ @@ -232,6 +233,25 @@ object json { } } + 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) + } } -- cgit v1.2.3 From ba2c214d2f8f9a1ba6d8265c55476a5281b8fd22 Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 20 Oct 2017 14:21:43 -0700 Subject: [RFC] Using "Refined" library (https://github.com/fthomas/refined) to allow defining entities with more precise types --- src/main/scala/xyz/driver/core/generators.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index f3913e5..e6eb654 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -39,6 +39,9 @@ object generators { def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) + def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] = + NonEmptyName[T](nextNonEmptyString(maxLength)) + def nextUuid(): UUID = java.util.UUID.randomUUID def nextRevision[T](): Revision[T] = Revision[T](nextUuid().toString) -- cgit v1.2.3