From cc86f8d609969b40793a227b9af4b41a18657dfb Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 22 Mar 2018 15:47:42 -0700 Subject: Add blob storage abstractions --- .../scala/xyz/driver/core/BlobStorageTest.scala | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/test/scala/xyz/driver/core/BlobStorageTest.scala (limited to 'src/test') diff --git a/src/test/scala/xyz/driver/core/BlobStorageTest.scala b/src/test/scala/xyz/driver/core/BlobStorageTest.scala new file mode 100644 index 0000000..65f9cbc --- /dev/null +++ b/src/test/scala/xyz/driver/core/BlobStorageTest.scala @@ -0,0 +1,88 @@ +package xyz.driver.core + +import java.nio.file.Files + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import akka.stream.scaladsl._ +import akka.util.ByteString +import org.scalatest._ +import org.scalatest.concurrent.ScalaFutures +import xyz.driver.core.storage.{BlobStorage, FileSystemBlobStorage} + +import scala.concurrent.Future +import scala.concurrent.duration._ + +class BlobStorageTest extends FlatSpec with ScalaFutures { + + implicit val patientce = PatienceConfig(timeout = 100.seconds) + + implicit val system = ActorSystem("blobstorage-test") + implicit val mat = ActorMaterializer() + import system.dispatcher + + def storageBehaviour(storage: BlobStorage) = { + val key = "foo" + val data = "hello world".getBytes + it should "upload data" in { + assert(storage.exists(key).futureValue === false) + assert(storage.uploadContent(key, data).futureValue === key) + assert(storage.exists(key).futureValue === true) + } + it should "download data" in { + val content = storage.content(key).futureValue + assert(content.isDefined) + assert(content.get === data) + } + it should "not download non-existing data" in { + assert(storage.content("bar").futureValue.isEmpty) + } + it should "overwrite an existing key" in { + val newData = "new string".getBytes("utf-8") + assert(storage.uploadContent(key, newData).futureValue === key) + assert(storage.content(key).futureValue.get === newData) + } + it should "upload a file" in { + val tmp = Files.createTempFile("testfile", "txt") + Files.write(tmp, data) + assert(storage.uploadFile(key, tmp).futureValue === key) + Files.delete(tmp) + } + it should "upload content" in { + val text = "foobar" + val src = Source + .single(text) + .map(l => ByteString(l)) + src.runWith(storage.upload(key).futureValue).futureValue + assert(storage.content(key).futureValue.map(_.toSeq) === Some("foobar".getBytes.toSeq)) + } + it should "delete content" in { + assert(storage.exists(key).futureValue) + storage.delete(key).futureValue + assert(!storage.exists(key).futureValue) + } + it should "download content" in { + storage.uploadContent(key, data) futureValue + val srcOpt = storage.download(key).futureValue + assert(srcOpt.isDefined) + val src = srcOpt.get + val content: Future[Array[Byte]] = src.runWith(Sink.fold(Array[Byte]())(_ ++ _)) + assert(content.futureValue === data) + } + it should "list keys" in { + assert(storage.list("").futureValue === Set(key)) + storage.uploadContent("a/a.txt", data).futureValue + storage.uploadContent("a/b", data).futureValue + storage.uploadContent("c/d", data).futureValue + storage.uploadContent("d", data).futureValue + assert(storage.list("").futureValue === Set(key, "a", "c", "d")) + assert(storage.list("a").futureValue === Set("a/a.txt", "a/b")) + assert(storage.list("a").futureValue === Set("a/a.txt", "a/b")) + assert(storage.list("c").futureValue === Set("c/d")) + } + } + + "File system storage" should behave like storageBehaviour( + new FileSystemBlobStorage(Files.createTempDirectory("test"))) + +} -- cgit v1.2.3 From 57ee8fa785c3815e45e473e5625d6e3cb1cd9402 Mon Sep 17 00:00:00 2001 From: Sergey Nastich Date: Mon, 9 Apr 2018 16:53:45 -0700 Subject: Add convenience methods to work with Tags: `taggedWith` wrapper and a proxy JSON format (#147) * Add convenience methods to work with Tags: `tagged` wrapper and a proxy JSON format --- src/main/scala/xyz/driver/core/core.scala | 4 ++++ src/main/scala/xyz/driver/core/json.scala | 12 ++++++++++-- src/test/scala/xyz/driver/core/JsonTest.scala | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) (limited to 'src/test') diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index be19f0f..14e1b10 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -25,6 +25,10 @@ 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] diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 06a8837..e7efce6 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -34,16 +34,24 @@ object json { } } - implicit def idFormat[T] = new RootJsonFormat[Id[T]] { + implicit def idFormat[T]: JsonFormat[Id[T]] = new JsonFormat[Id[T]] { def write(id: Id[T]) = JsString(id.value) - def read(value: JsValue) = value match { + def read(value: JsValue): Id[T] = value match { case JsString(id) if Try(UUID.fromString(id)).isSuccess => Id[T](id.toLowerCase) case JsString(id) => Id[T](id) case _ => throw DeserializationException("Id expects string") } } + implicit def taggedFormat[F, T](implicit underlying: JsonFormat[F]): JsonFormat[F @@ T] = new JsonFormat[F @@ T] { + import tagging._ + + override def write(obj: F @@ T): JsValue = underlying.write(obj) + + override def read(json: JsValue): F @@ T = underlying.read(json).tagged[T] + } + 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/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala index 7e8dba2..b845a44 100644 --- a/src/test/scala/xyz/driver/core/JsonTest.scala +++ b/src/test/scala/xyz/driver/core/JsonTest.scala @@ -13,6 +13,7 @@ import spray.json._ import xyz.driver.core.TestTypes.CustomGADT import xyz.driver.core.domain.{Email, PhoneNumber} import xyz.driver.core.json.enumeratum.HasJsonFormat +import xyz.driver.core.tagging.Taggable import xyz.driver.core.time.TimeOfDay import scala.collection.immutable.IndexedSeq @@ -31,6 +32,19 @@ class JsonTest extends FlatSpec with Matchers { parsedId should be(referenceId) } + "Json format for @@" should "read and write correct JSON" in { + trait Irrelevant + val reference = Id[JsonTest]("SomeID").tagged[Irrelevant] + + val format = json.taggedFormat[Id[JsonTest], Irrelevant] + + val writtenJson = format.write(reference) + writtenJson shouldBe JsString("SomeID") + + val parsedId: Id[JsonTest] @@ Irrelevant = format.read(writtenJson) + parsedId shouldBe reference + } + "Json format for Name" should "read and write correct JSON" in { val referenceName = Name[String]("Homer") -- cgit v1.2.3