aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Nastich <nastich@users.noreply.github.com>2018-09-12 16:10:17 -0400
committerGitHub <noreply@github.com>2018-09-12 16:10:17 -0400
commit8fef53d44a57008dea411b882b12bc3d5d1ca2e0 (patch)
treed2304188d61bdd39ee4c494f3e309c209908592a
parentccd6c4281fb0ddd0a74b79aa24f85834f5c9647a (diff)
downloaddriver-core-8fef53d44a57008dea411b882b12bc3d5d1ca2e0.tar.gz
driver-core-8fef53d44a57008dea411b882b12bc3d5d1ca2e0.tar.bz2
driver-core-8fef53d44a57008dea411b882b12bc3d5d1ca2e0.zip
Add `Trimmed` tag and its logic (revisited) (#215)v1.14.2
* Add option and iterable converters for transparent `@@ Trimmed` creation. * Move tagging stuff to a separate package - relieve `core.scala` from some extra code. * Add Tagging stuff and publishing section to README.md
-rw-r--r--README.md40
-rw-r--r--src/main/scala/xyz/driver/core/core.scala25
-rw-r--r--src/main/scala/xyz/driver/core/tagging/tagging.scala62
-rw-r--r--src/test/scala/xyz/driver/core/BlobStorageTest.scala1
-rw-r--r--src/test/scala/xyz/driver/core/CoreTest.scala12
-rw-r--r--src/test/scala/xyz/driver/core/FileTest.scala48
-rw-r--r--src/test/scala/xyz/driver/core/JsonTest.scala11
-rw-r--r--src/test/scala/xyz/driver/core/rest/DriverAppTest.scala1
-rw-r--r--src/test/scala/xyz/driver/core/tagging/TaggingTest.scala63
9 files changed, 204 insertions, 59 deletions
diff --git a/README.md b/README.md
index 952961f..e65edf5 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,8 @@ Core library is used to provide ways to implement practices established in [Driv
## Components
- * `core package` provides `Id` and `Name` implementations (with equal and ordering), utils for ScalaZ `OptionT`, and also `make` and `using` functions,
+ * `core package` provides `Id` and `Name` implementations (with equal and ordering), utils for ScalaZ `OptionT`, and also `make` and `using` functions and `@@` (tagged) type,
+ * `tagging` Utilities for tagging primitive types for extra type safety, as well as some tags that involve extra transformation upon deserializing with spray,
* `config` Contains method `loadDefaultConfig` with default way of providing config to the application,
* `domain` Common generic domain objects, e.g., `Email` and `PhoneNumber`,
* `messages` Localization messages supporting different locales and methods to read from config,
@@ -75,8 +76,38 @@ for {
} yield x
```
+### `@@` or Tagged types
+
+For type definitions, the only import required is
+
+```scala
+import xyz.driver.core.@@
+```
+
+which provides just the ability to tag types: `val value: String @@ Important`. Two `String`s with different tags will
+be distinguished by the compiler, helping reduce the possibility of mixing values passed into methods with several
+arguments of identical types.
+
+To work with tags in actual values, use the following convenience methods:
+
+```scala
+import xyz.driver.core.tagging._
+
+val str = "abc".tagged[Important]
+```
+
+or go back to plain (say, in case you have an implicit for untagged value)
+
+```scala
+// val trimmedExternalId: String @@ Trimmed = "123"
+
+Trials.filter(_.externalId === trimmedExternalId.untag)
+```
+
### `Time` and `TimeProvider`
+**NOTE: The contents of this section has been deprecated - use java.time.Clock instead**
+
Usage examples for `Time` (also check [TimeTest](https://github.com/drivergroup/driver-core/blob/master/src/test/scala/xyz/driver/core/TimeTest.scala) for more examples).
Time(234L).isAfter(Time(123L))
@@ -175,8 +206,7 @@ Stats it gives access to are,
2. Publish to local repository, to use changes in depending projects:
- $ sbt publish-local
-
-3. Release a new version of core:
+ $ sbt publishLocal
- $ sbt release
+3. In order to release a new version of core, merge your PR, tag the HEAD master commit with the next version
+ (don't forget the "v." prefix) and push tags - Travis will release the artifact automatically!
diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala
index 846bed3..2ab4e88 100644
--- a/src/main/scala/xyz/driver/core/core.scala
+++ b/src/main/scala/xyz/driver/core/core.scala
@@ -1,16 +1,15 @@
package xyz.driver
-import scalaz.{Equal, Monad, OptionT}
import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.collection.NonEmpty
+import scalaz.{Equal, Monad, OptionT}
import xyz.driver.core.rest.errors.ExternalServiceException
+import xyz.driver.core.tagging.Tagged
import scala.concurrent.{ExecutionContext, Future}
// TODO: this package seems too complex, look at all the features we need!
-import scala.language.reflectiveCalls
-import scala.language.higherKinds
-import scala.language.implicitConversions
+import scala.language.{higherKinds, implicitConversions, reflectiveCalls}
package object core {
@@ -29,14 +28,7 @@ package object core {
}
}
- object tagging {
- private[core] trait Tagged[+V, +Tag]
-
- implicit class Taggable[V <: Any](val v: V) extends AnyVal {
- def tagged[Tag]: V @@ Tag = v.asInstanceOf[V @@ Tag]
- }
- }
- type @@[+V, +Tag] = V with tagging.Tagged[V, Tag]
+ type @@[+V, +Tag] = V with Tagged[V, Tag]
implicit class OptionTExtensions[H[_]: Monad, T](optionTValue: OptionT[H, T]) {
@@ -129,13 +121,4 @@ package core {
final case class Base64(value: String)
- trait Trimmed
-
- object Trimmed {
- import tagging._
-
- implicit def string2Trimmed(str: String): String @@ Trimmed = str.trim().tagged[Trimmed]
-
- implicit def name2Trimmed[T](name: Name[T]): Name[T] @@ Trimmed = Name[T](name.value.trim()).tagged[Trimmed]
- }
}
diff --git a/src/main/scala/xyz/driver/core/tagging/tagging.scala b/src/main/scala/xyz/driver/core/tagging/tagging.scala
new file mode 100644
index 0000000..5b6599e
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/tagging/tagging.scala
@@ -0,0 +1,62 @@
+package xyz.driver.core
+
+import scala.collection.generic.CanBuildFrom
+import scala.language.{higherKinds, implicitConversions}
+
+/**
+ * @author sergey
+ * @since 9/11/18
+ */
+package object tagging {
+
+ implicit class Taggable[V <: Any](val v: V) extends AnyVal {
+ def tagged[Tag]: V @@ Tag = v.asInstanceOf[V @@ Tag]
+ }
+
+}
+
+package tagging {
+
+ sealed trait Tagged[+V, +Tag]
+
+ object Tagged {
+ implicit class TaggedOps[V, Tag](val v: V @@ Tag) extends AnyVal {
+ def tagless: V = v
+ }
+
+ implicit def orderingMagnet[V, Tag](implicit ord: Ordering[V]): Ordering[V @@ Tag] =
+ ord.asInstanceOf[Ordering[V @@ Tag]]
+
+ }
+
+ sealed trait Trimmed
+
+ object Trimmed {
+
+ implicit def apply[V](trimmable: V)(implicit ev: CanBeTrimmed[V]): V @@ Trimmed = {
+ ev.trim(trimmable).tagged[Trimmed]
+ }
+
+ sealed trait CanBeTrimmed[T] {
+ def trim(trimmable: T): T
+ }
+
+ implicit object StringCanBeTrimmed extends CanBeTrimmed[String] {
+ def trim(str: String): String = str.trim()
+ }
+
+ implicit def nameCanBeTrimmed[T]: CanBeTrimmed[Name[T]] = new CanBeTrimmed[Name[T]] {
+ def trim(name: Name[T]): Name[T] = Name[T](name.value.trim())
+ }
+
+ implicit def option2Trimmed[V: CanBeTrimmed](option: Option[V]): Option[V @@ Trimmed] =
+ option.map(Trimmed(_))
+
+ implicit def coll2Trimmed[T, C[_] <: Traversable[_]](coll: C[T])(
+ implicit ev: C[T] <:< Traversable[T],
+ tr: CanBeTrimmed[T],
+ bf: CanBuildFrom[Nothing, T @@ Trimmed, C[T @@ Trimmed]]): C[T @@ Trimmed] =
+ ev(coll).map(Trimmed(_)(tr)).to[C]
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/core/BlobStorageTest.scala b/src/test/scala/xyz/driver/core/BlobStorageTest.scala
index 637f9e0..811cc60 100644
--- a/src/test/scala/xyz/driver/core/BlobStorageTest.scala
+++ b/src/test/scala/xyz/driver/core/BlobStorageTest.scala
@@ -12,6 +12,7 @@ import xyz.driver.core.storage.{BlobStorage, FileSystemBlobStorage}
import scala.concurrent.Future
import scala.concurrent.duration._
+import scala.language.postfixOps
class BlobStorageTest extends FlatSpec with ScalaFutures {
diff --git a/src/test/scala/xyz/driver/core/CoreTest.scala b/src/test/scala/xyz/driver/core/CoreTest.scala
index d280d73..f448d24 100644
--- a/src/test/scala/xyz/driver/core/CoreTest.scala
+++ b/src/test/scala/xyz/driver/core/CoreTest.scala
@@ -34,8 +34,10 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar {
(Id[String]("1234213") === Id[String]("213414")) should be(false)
(Id[String]("213414") === Id[String]("1234213")) 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")))
+ val ids = Seq(Id[String]("4"), Id[String]("3"), Id[String]("2"), Id[String]("1"))
+ val sorted = Seq(Id[String]("1"), Id[String]("2"), Id[String]("3"), Id[String]("4"))
+
+ ids.sorted should contain theSameElementsInOrderAs sorted
}
it should "have type-safe conversions" in {
@@ -68,8 +70,9 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar {
(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")))
+ val names = Seq(Name[String]("d"), Name[String]("cc"), Name[String]("a"), Name[String]("bbb"))
+ val sorted = Seq(Name[String]("a"), Name[String]("bbb"), Name[String]("cc"), Name[String]("d"))
+ names.sorted should contain theSameElementsInOrderAs sorted
}
"Revision" should "have equality working correctly" in {
@@ -81,4 +84,5 @@ class CoreTest extends FlatSpec with Matchers with MockitoSugar {
(bla === foo) should be(false)
(foo === bla) should be(false)
}
+
}
diff --git a/src/test/scala/xyz/driver/core/FileTest.scala b/src/test/scala/xyz/driver/core/FileTest.scala
index 8728089..554a991 100644
--- a/src/test/scala/xyz/driver/core/FileTest.scala
+++ b/src/test/scala/xyz/driver/core/FileTest.scala
@@ -62,17 +62,17 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
val s3Storage = new S3Storage(amazonS3Mock, testBucket, scala.concurrent.ExecutionContext.global)
- val filesBefore = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ val filesBefore = Await.result(s3Storage.list(testDirPath).run, 10.seconds)
filesBefore shouldBe empty
- val fileExistsBeforeUpload = Await.result(s3Storage.exists(testFilePath), 10 seconds)
+ val fileExistsBeforeUpload = Await.result(s3Storage.exists(testFilePath), 10.seconds)
fileExistsBeforeUpload should be(false)
- Await.result(s3Storage.upload(sourceTestFile, testFilePath), 10 seconds)
+ Await.result(s3Storage.upload(sourceTestFile, testFilePath), 10.seconds)
- val filesAfterUpload = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ val filesAfterUpload = Await.result(s3Storage.list(testDirPath).run, 10.seconds)
filesAfterUpload.size should be(1)
- val fileExistsAfterUpload = Await.result(s3Storage.exists(testFilePath), 10 seconds)
+ val fileExistsAfterUpload = Await.result(s3Storage.exists(testFilePath), 10.seconds)
fileExistsAfterUpload should be(true)
val uploadedFileLine = filesAfterUpload.head
uploadedFileLine.name should be(Name[File](testFileName))
@@ -80,15 +80,15 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
uploadedFileLine.revision.id.length should be > 0
uploadedFileLine.lastModificationDate.millis should be > 0L
- val downloadedFile = Await.result(s3Storage.download(testFilePath).run, 10 seconds)
+ val downloadedFile = Await.result(s3Storage.download(testFilePath).run, 10.seconds)
downloadedFile shouldBe defined
downloadedFile.foreach {
_.getAbsolutePath.endsWith(testFilePath.toString) should be(true)
}
- Await.result(s3Storage.delete(testFilePath), 10 seconds)
+ Await.result(s3Storage.delete(testFilePath), 10.seconds)
- val filesAfterRemoval = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ val filesAfterRemoval = Await.result(s3Storage.list(testDirPath).run, 10.seconds)
filesAfterRemoval shouldBe empty
}
@@ -103,18 +103,18 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
val fileStorage = new FileSystemStorage(scala.concurrent.ExecutionContext.global)
- val filesBefore = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ val filesBefore = Await.result(fileStorage.list(testDirPath).run, 10.seconds)
filesBefore shouldBe empty
- val fileExistsBeforeUpload = Await.result(fileStorage.exists(testFilePath), 10 seconds)
+ val fileExistsBeforeUpload = Await.result(fileStorage.exists(testFilePath), 10.seconds)
fileExistsBeforeUpload should be(false)
- Await.result(fileStorage.upload(sourceTestFile, testFilePath), 10 seconds)
+ Await.result(fileStorage.upload(sourceTestFile, testFilePath), 10.seconds)
- val filesAfterUpload = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ val filesAfterUpload = Await.result(fileStorage.list(testDirPath).run, 10.seconds)
filesAfterUpload.size should be(1)
- val fileExistsAfterUpload = Await.result(fileStorage.exists(testFilePath), 10 seconds)
+ val fileExistsAfterUpload = Await.result(fileStorage.exists(testFilePath), 10.seconds)
fileExistsAfterUpload should be(true)
val uploadedFileLine = filesAfterUpload.head
@@ -123,13 +123,13 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
uploadedFileLine.revision.id.length should be > 0
uploadedFileLine.lastModificationDate.millis should be > 0L
- val downloadedFile = Await.result(fileStorage.download(testFilePath).run, 10 seconds)
+ val downloadedFile = Await.result(fileStorage.download(testFilePath).run, 10.seconds)
downloadedFile shouldBe defined
downloadedFile.map(_.getAbsolutePath) should be(Some(testFilePath.toString))
- Await.result(fileStorage.delete(testFilePath), 10 seconds)
+ Await.result(fileStorage.delete(testFilePath), 10.seconds)
- val filesAfterRemoval = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ val filesAfterRemoval = Await.result(fileStorage.list(testDirPath).run, 10.seconds)
filesAfterRemoval shouldBe empty
}
@@ -174,10 +174,10 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
blobMock // after file is uploaded
)
- val filesBefore = Await.result(gcsStorage.list(testDirPath).run, 10 seconds)
+ val filesBefore = Await.result(gcsStorage.list(testDirPath).run, 10.seconds)
filesBefore shouldBe empty
- val fileExistsBeforeUpload = Await.result(gcsStorage.exists(testFilePath), 10 seconds)
+ val fileExistsBeforeUpload = Await.result(gcsStorage.exists(testFilePath), 10.seconds)
fileExistsBeforeUpload should be(false)
when(gcsMock.get(testBucket.value)).thenReturn(bucketMock)
@@ -185,23 +185,23 @@ class FileTest extends FlatSpec with Matchers with MockitoSugar {
when(bucketMock.create(org.mockito.Matchers.eq(testFileName), any[FileInputStream], any[BlobWriteOption]))
.thenReturn(blobMock)
- Await.result(gcsStorage.upload(sourceTestFile, testFilePath), 10 seconds)
+ Await.result(gcsStorage.upload(sourceTestFile, testFilePath), 10.seconds)
- val filesAfterUpload = Await.result(gcsStorage.list(testDirPath).run, 10 seconds)
+ val filesAfterUpload = Await.result(gcsStorage.list(testDirPath).run, 10.seconds)
filesAfterUpload.size should be(1)
- val fileExistsAfterUpload = Await.result(gcsStorage.exists(testFilePath), 10 seconds)
+ val fileExistsAfterUpload = Await.result(gcsStorage.exists(testFilePath), 10.seconds)
fileExistsAfterUpload should be(true)
- val downloadedFile = Await.result(gcsStorage.download(testFilePath).run, 10 seconds)
+ val downloadedFile = Await.result(gcsStorage.download(testFilePath).run, 10.seconds)
downloadedFile shouldBe defined
downloadedFile.foreach {
_.getAbsolutePath should endWith(testFilePath.toString)
}
- Await.result(gcsStorage.delete(testFilePath), 10 seconds)
+ Await.result(gcsStorage.delete(testFilePath), 10.seconds)
- val filesAfterRemoval = Await.result(gcsStorage.list(testDirPath).run, 10 seconds)
+ val filesAfterRemoval = Await.result(gcsStorage.list(testDirPath).run, 10.seconds)
filesAfterRemoval shouldBe empty
}
diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala
index 9a079b2..2aa3572 100644
--- a/src/test/scala/xyz/driver/core/JsonTest.scala
+++ b/src/test/scala/xyz/driver/core/JsonTest.scala
@@ -18,11 +18,12 @@ import xyz.driver.core.auth.AuthCredentials
import xyz.driver.core.domain.{Email, PhoneNumber}
import xyz.driver.core.json._
import xyz.driver.core.json.enumeratum.HasJsonFormat
-import xyz.driver.core.tagging.Taggable
+import xyz.driver.core.tagging._
import xyz.driver.core.time.provider.SystemTimeProvider
import xyz.driver.core.time.{Time, TimeOfDay}
import scala.collection.immutable.IndexedSeq
+import scala.language.postfixOps
class JsonTest extends WordSpec with Matchers with Inspectors {
import DefaultJsonProtocol._
@@ -55,10 +56,12 @@ class JsonTest extends WordSpec with Matchers with Inspectors {
}
"read and write correct JSON when there's an implicit conversion defined" in {
- JsString(" some string ").convertTo[String @@ Trimmed] shouldBe "some string"
+ val input = " some string "
- val trimmed: String @@ Trimmed = " some string "
- trimmed.toJson shouldBe JsString("some string")
+ JsString(input).convertTo[String @@ Trimmed] shouldBe input.trim()
+
+ val trimmed: String @@ Trimmed = input
+ trimmed.toJson shouldBe JsString(trimmed)
}
}
diff --git a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala b/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala
index 8f552db..118024a 100644
--- a/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala
+++ b/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala
@@ -1,6 +1,5 @@
package xyz.driver.core.rest
-import akka.http.scaladsl.model.headers.CacheDirectives.`no-cache`
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.{HttpMethod, StatusCodes}
import akka.http.scaladsl.server.{Directives, Route}
diff --git a/src/test/scala/xyz/driver/core/tagging/TaggingTest.scala b/src/test/scala/xyz/driver/core/tagging/TaggingTest.scala
new file mode 100644
index 0000000..14dfaf9
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/tagging/TaggingTest.scala
@@ -0,0 +1,63 @@
+package xyz.driver.core.tagging
+
+import org.scalatest.{Matchers, WordSpec}
+import xyz.driver.core.{@@, Name}
+
+/**
+ * @author sergey
+ * @since 9/11/18
+ */
+class TaggingTest extends WordSpec with Matchers {
+
+ "@@ Trimmed" should {
+ "produce values transparently from Strings and Names (by default)" in {
+ val s: String @@ Trimmed = " trimmed "
+ val n: Name[Int] @@ Trimmed = Name(" trimmed ")
+
+ s shouldBe "trimmed"
+ n shouldBe Name[Int]("trimmed")
+ }
+
+ "produce values transparently from values that have an implicit conversion defined" in {
+ import scala.language.implicitConversions
+ implicit def stringSeq2Trimmed(stringSeq: Seq[String]): Seq[String] @@ Trimmed =
+ stringSeq.map(_.trim()).tagged[Trimmed]
+
+ val strings: Seq[String] @@ Trimmed = Seq(" trimmed1 ", " trimmed2 ")
+ strings shouldBe Seq("trimmed1", "trimmed2")
+ }
+
+ "produce values transparently from Options of values that have Trimmed implicits" in {
+ val maybeStringDirect: Option[String @@ Trimmed] = Some(" trimmed ")
+ val maybeStringFromMap: Option[String @@ Trimmed] = Map("s" -> " trimmed ").get("s")
+
+ val maybeNameDirect: Option[Name[Int] @@ Trimmed] = Some(Name(" trimmed "))
+ val maybeNameFromMap: Option[Name[Int] @@ Trimmed] = Map("s" -> Name[Int](" trimmed ")).get("s")
+
+ maybeStringDirect shouldBe Some("trimmed")
+ maybeStringFromMap shouldBe Some("trimmed")
+ maybeNameDirect shouldBe Some(Name[Int]("trimmed"))
+ maybeNameFromMap shouldBe Some(Name[Int]("trimmed"))
+ }
+
+ "produce values transparently from collections of values that have Trimmed implicits" in {
+ val strings = Seq("s" -> " trimmed1 ", "s" -> " trimmed2 ")
+ val names = strings.map {
+ case (k, v) => k -> Name[Int](v)
+ }
+
+ val trimmedStrings: Seq[String @@ Trimmed] = strings.groupBy(_._1)("s").map(_._2)
+ val trimmedNames: Seq[Name[Int] @@ Trimmed] = names.groupBy(_._1)("s").map(_._2)
+
+ trimmedStrings shouldBe Seq("trimmed1", "trimmed2")
+ trimmedNames shouldBe Seq("trimmed1", "trimmed2").map(Name[Int])
+ }
+
+ "have Ordering" in {
+ val names: Seq[Name[Int] @@ Trimmed] = Seq(" 2 ", " 1 ", "3").map(Name[Int])
+
+ names.sorted should contain inOrderOnly (Name("1"), Name("2"), Name("3"))
+ }
+ }
+
+}