diff --git a/.gitignore b/.gitignore index 2ec5078..8aef1f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target .classpath .project .settings +.cache-* diff --git a/README.md b/README.md index d6136a6..22a194c 100644 --- a/README.md +++ b/README.md @@ -139,21 +139,31 @@ The execution model leverages on Applicative Functors to express the independenc # Getting started # -To use clump, just add the dependency to the project's build configuration. There are two versions of the project: +To use clump, just add the dependency to the project's build configuration. There are three versions of the project: -1. `clump-scala`, that uses Scala Futures and doesn't have external dependencies. -2. `clump-twitter`, that uses Twitter Futures and has the dependency to `twitter-util`. +1. `clump-scalaJVM`, that uses Scala Futures and doesn't have external dependencies. +2. `clump-scalaJS`, for usage with ScalaJS. +3. `clump-twitter`, that uses Twitter Futures and has the dependency to `twitter-util`. __Important__: Change ```x.x.x``` with the latest version listed by the [CHANGELOG.md](https://github.com/getclump/clump/blob/master/CHANGELOG.md) file. SBT +clump-scalaJVM ```scala libraryDependencies ++= Seq( "io.getclump" %% "clump-scala" % "x.x.x" ) ``` +clump-scalaJS +```scala +libraryDependencies ++= Seq( + "io.getclump" %%% "clump-scala" % "x.x.x" +) +``` + +clump-twitter ```scala libraryDependencies ++= Seq( "io.getclump" %% "clump-twitter" % "x.x.x" diff --git a/project/Build.scala b/project/Build.scala index 732f44c..1b3cb4e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2,16 +2,16 @@ import com.typesafe.sbt.pgp.PgpKeys import sbt.Keys._ import sbt._ import sbtrelease.ReleasePlugin._ +import org.scalajs.sbtplugin.cross.CrossProject +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ object Build extends Build { val commonSettings = Seq( organization := "io.getclump", - scalaVersion := "2.10.4", - crossScalaVersions := Seq("2.10.4", "2.11.5"), - libraryDependencies ++= Seq( - "org.specs2" %% "specs2" % "2.4.2" % "test", - "org.mockito" % "mockito-core" % "1.9.5" % "test" - ), + scalaVersion := "2.11.6", + crossScalaVersions := Seq("2.10.4", "2.11.6"), + libraryDependencies += "com.lihaoyi" %%% "utest" % "0.3.1", + testFrameworks += new TestFramework("utest.runner.Framework"), scalacOptions ++= Seq( "-deprecation", "-encoding", "UTF-8", @@ -58,22 +58,34 @@ object Build extends Build { ) - lazy val clumpScala = Project(id = "clump-scala", base = file(".")) - .settings(name := "clump-scala") - .settings(commonSettings: _*) - .settings(target <<= target(_ / "clump-scala")) - .aggregate(clumpTwitter) + lazy val clump = + Project(id = "clump", base = file(".")) + .settings(scalaSource in Test := file("root")) + .settings(scalaSource in Compile := file("root")) + .settings(publish := { }) + .aggregate(clumpScalaJs, clumpScalaJvm, clumpTwitter) - lazy val clumpTwitter = Project(id = "clump-twitter", base = file(".")) - .settings(name := "clump-twitter") - .settings(commonSettings: _*) - .settings(libraryDependencies += "com.twitter" %% "util-core" % "6.22.0") - .settings(target <<= target(_ / "clump-twitter")) - .settings(excludeFilter in unmanagedSources := "package.scala") - .settings(sourceGenerators in Compile += Def.task { - val source = sourceDirectory.value / "main" / "scala" / "io" / "getclump" / "package-twitter.scala.tmpl" - val file = sourceManaged.value / "main" / "scala" / "io" / "getclump" / "package.scala" - IO.copyFile(source, file) - Seq(file) - }.taskValue) + + lazy val clumpScala: CrossProject = + CrossProject(id = "clump-scala", base = file("."), CrossType.Pure) + .settings(name := "clump-scala") + .settings(commonSettings: _*) + .settings(target <<= target(_ / "clump-scala")) + + lazy val clumpScalaJvm = clumpScala.jvm.aggregate(clumpScalaJs) + lazy val clumpScalaJs = clumpScala.js + + lazy val clumpTwitter = + Project(id = "clump-twitter", base = file(".")) + .settings(name := "clump-twitter") + .settings(commonSettings: _*) + .settings(libraryDependencies += "com.twitter" %% "util-core" % "6.22.0") + .settings(target <<= target(_ / "clump-twitter")) + .settings(excludeFilter in unmanagedSources := "package.scala") + .settings(sourceGenerators in Compile += Def.task { + val source = sourceDirectory.value / "main" / "scala" / "io" / "getclump" / "package-twitter.scala.tmpl" + val file = sourceManaged.value / "main" / "scala" / "io" / "getclump" / "package.scala" + IO.copyFile(source, file) + Seq(file) + }.taskValue) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 2a023a8..4750a1d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -14,4 +14,6 @@ addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") addSbtPlugin("com.github.gseitz" % "sbt-release" % "0.8.5") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") \ No newline at end of file +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3") diff --git a/src/main/scala/io/getclump/package-twitter.scala.tmpl b/src/main/scala/io/getclump/package-twitter.scala.tmpl index f1099ca..5320981 100644 --- a/src/main/scala/io/getclump/package-twitter.scala.tmpl +++ b/src/main/scala/io/getclump/package-twitter.scala.tmpl @@ -27,6 +27,4 @@ package object getclump { def sequence[T](futures: Seq[Future[T]]) = Future.collect(futures) } - private[getclump] def awaitResult[T](future: Future[T]) = - com.twitter.util.Await.result(future) } diff --git a/src/main/scala/io/getclump/package.scala b/src/main/scala/io/getclump/package.scala index 5ae0848..ec869fa 100644 --- a/src/main/scala/io/getclump/package.scala +++ b/src/main/scala/io/getclump/package.scala @@ -10,6 +10,4 @@ package object getclump { private[getclump]type Future[+T] = scala.concurrent.Future[T] private[getclump] val Future = scala.concurrent.Future - private[getclump] def awaitResult[T](future: Future[T]) = - scala.concurrent.Await.result(future, scala.concurrent.duration.Duration.Inf) } diff --git a/src/test/scala/io/getclump/ClumpApiSpec.scala b/src/test/scala/io/getclump/ClumpApiSpec.scala index 38bc23c..f7391e1 100644 --- a/src/test/scala/io/getclump/ClumpApiSpec.scala +++ b/src/test/scala/io/getclump/ClumpApiSpec.scala @@ -1,372 +1,392 @@ package io.getclump -import org.junit.runner.RunWith -import org.specs2.runner.JUnitRunner +import utest._ -@RunWith(classOf[JUnitRunner]) -class ClumpApiSpec extends Spec { +object ClumpApiSpec extends Spec { - "the Clump object" >> { + val tests = TestSuite { - "allows to create a constant clump" >> { + "the Clump object" - { - "from a future (Clump.future)" >> { + "allows to create a constant clump" - { - "success" >> { - "optional" >> { - "defined" in { - clumpResult(Clump.future(Future.successful(Some(1)))) mustEqual Some(1) + "from a future (Clump.future)" - { + + "success" - { + "optional" - { + "defined" - { + assertResult(Clump.future(Future.successful(Some(1))), Some(1)) + } + "undefined" - { + assertResult(Clump.future(Future.successful(None)), None) + } } - "undefined" in { - clumpResult(Clump.future(Future.successful(None))) mustEqual None + "non-optional" - { + assertResult(Clump.future(Future.successful(1)), Some(1)) } } - "non-optional" in { - clumpResult(Clump.future(Future.successful(1))) mustEqual Some(1) + + "failure" - { + assertFailure[IllegalStateException] { + Clump.future(Future.failed(new IllegalStateException)) + } } } - "failure" in { - clumpResult(Clump.future(Future.failed(new IllegalStateException))) must throwA[IllegalStateException] - } - } + "from a value (Clump.apply)" - { + "propogates exceptions" - { + val clump = Clump { throw new IllegalStateException } + assertFailure[IllegalStateException] { + clump + } + } - "from a value (Clump.apply)" >> { - "propogates exceptions" in { - val clump = Clump { throw new IllegalStateException } - clumpResult(clump) must throwA[IllegalStateException] + "no exception" - { + assertResult(Clump(1), Some(1)) + } } - "no exception" in { - clumpResult(Clump(1)) mustEqual Some(1) + "from a value (Clump.value)" - { + assertResult(Clump.value(1), Some(1)) } - } - "from a value (Clump.value)" in { - clumpResult(Clump.value(1)) mustEqual Some(1) - } + "from a value (Clump.successful)" - { + assertResult(Clump.successful(1), Some(1)) + } - "from a value (Clump.successful)" in { - clumpResult(Clump.successful(1)) mustEqual Some(1) - } + "from an option (Clump.value)" - { - "from an option (Clump.value)" >> { + "defined" - { + assertResult(Clump.value(Option(1)), Option(1)) + } - "defined" in { - clumpResult(Clump.value(Option(1))) mustEqual Option(1) + "empty" - { + assertResult(Clump.value(None), None) + } } - "empty" in { - clumpResult(Clump.value(None)) mustEqual None + "failed (Clump.exception)" - { + assertFailure[IllegalStateException] { + Clump.exception(new IllegalStateException) + } } - } - "failed (Clump.exception)" in { - clumpResult(Clump.exception(new IllegalStateException)) must throwA[IllegalStateException] + "failed (Clump.failed)" - { + assertFailure[IllegalStateException] { + Clump.failed(new IllegalStateException) + } + } } - "failed (Clump.failed)" in { - clumpResult(Clump.failed(new IllegalStateException)) must throwA[IllegalStateException] + "allows to create a clump traversing multiple inputs (Clump.traverse)" - { + "list" - { + val inputs = List(1, 2, 3) + val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) + assertResult(clump, Some(List(2, 3, 4))) + } + "set" - { + val inputs = Set(1, 2, 3) + val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) + assertResult(clump, Some(Set(2, 3, 4))) + } + "seq" - { + val inputs = Seq(1, 2, 3) + val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) + assertResult(clump, Some(Seq(2, 3, 4))) + } } - } - "allows to create a clump traversing multiple inputs (Clump.traverse)" in { - "list" in { - val inputs = List(1, 2, 3) - val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) - clumpResult(clump) ==== Some(List(2, 3, 4)) - } - "set" in { - val inputs = Set(1, 2, 3) - val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) - clumpResult(clump) ==== Some(Set(2, 3, 4)) - } - "seq" in { - val inputs = Seq(1, 2, 3) - val clump = Clump.traverse(inputs)(i => Clump.value(i + 1)) - clumpResult(clump) ==== Some(Seq(2, 3, 4)) + "allows to collect multiple clumps - only one (Clump.collect)" - { + "list" - { + val clumps = List(Clump.value(1), Clump.value(2)) + assertResult(Clump.collect(clumps), Some(List(1, 2))) + } + "set" - { + val clumps = Set(Clump.value(1), Clump.value(2)) + assertResult(Clump.collect(clumps), Some(Set(1, 2))) + } + "seq" - { + val clumps = Seq(Clump.value(1), Clump.value(2)) + assertResult(Clump.collect(clumps), Some(Seq(1, 2))) + } } - } - "allows to collect multiple clumps in only one (Clump.collect)" >> { - "list" in { - val clumps = List(Clump.value(1), Clump.value(2)) - clumpResult(Clump.collect(clumps)) mustEqual Some(List(1, 2)) + "allows to create an empty Clump (Clump.empty)" - { + assertResult(Clump.empty, None) } - "set" in { - val clumps = Set(Clump.value(1), Clump.value(2)) - clumpResult(Clump.collect(clumps)) mustEqual Some(Set(1, 2)) - } - "seq" in { - val clumps = Seq(Clump.value(1), Clump.value(2)) - clumpResult(Clump.collect(clumps)) mustEqual Some(Seq(1, 2)) - } - } - "allows to create an empty Clump (Clump.empty)" in { - clumpResult(Clump.empty) ==== None - } - - "allows to join clumps" >> { + "allows to join clumps" - { - def c(int: Int) = Clump.value(int) + def c(int: Int) = Clump.value(int) - "2 instances" in { - val clump = Clump.join(c(1), c(2)) - clumpResult(clump) mustEqual Some(1, 2) - } - "3 instances" in { - val clump = Clump.join(c(1), c(2), c(3)) - clumpResult(clump) mustEqual Some(1, 2, 3) - } - "4 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4) - } - "5 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5) - } - "6 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5, 6) - } - "7 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5, 6, 7) - } - "8 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5, 6, 7, 8) - } - "9 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8), c(9)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5, 6, 7, 8, 9) - } - "10 instances" in { - val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8), c(9), c(10)) - clumpResult(clump) mustEqual Some(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + "2 instances" - { + val clump = Clump.join(c(1), c(2)) + assertResult(clump, Some(1, 2)) + } + "3 instances" - { + val clump = Clump.join(c(1), c(2), c(3)) + assertResult(clump, Some(1, 2, 3)) + } + "4 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4)) + assertResult(clump, Some(1, 2, 3, 4)) + } + "5 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5)) + assertResult(clump, Some(1, 2, 3, 4, 5)) + } + "6 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6)) + assertResult(clump, Some(1, 2, 3, 4, 5, 6)) + } + "7 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7)) + assertResult(clump, Some(1, 2, 3, 4, 5, 6, 7)) + } + "8 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8)) + assertResult(clump, Some(1, 2, 3, 4, 5, 6, 7, 8)) + } + "9 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8), c(9)) + assertResult(clump, Some(1, 2, 3, 4, 5, 6, 7, 8, 9)) + } + "10 instances" - { + val clump = Clump.join(c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8), c(9), c(10)) + assertResult(clump, Some(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + } } } - } - "a Clump instance" >> { + "a Clump instance" - { - "can be mapped to a new clump" >> { + "can be mapped to a new clump" - { - "using simple a value transformation (clump.map)" in { - clumpResult(Clump.value(1).map(_ + 1)) mustEqual Some(2) - } - - "using a transformation that creates a new clump (clump.flatMap)" >> { - "both clumps are defined" in { - clumpResult(Clump.value(1).flatMap(i => Clump.value(i + 1))) mustEqual Some(2) + "using simple a value transformation (clump.map)" - { + assertResult(Clump.value(1).map(_ + 1), Some(2)) } - "initial clump is undefined" in { - clumpResult(Clump.value(None).flatMap(i => Clump.value(2))) mustEqual None + + "using a transformation that creates a new clump (clump.flatMap)" - { + "both clumps are defined" - { + assertResult(Clump.value(1).flatMap(i => Clump.value(i + 1)), Some(2)) + } + "initial clump is undefined" - { + assertResult(Clump.value(None).flatMap(i => Clump.value(2)), None) + } } } - } - "can be joined with another clump and produce a new clump with the value of both (clump.join)" >> { - "both clumps are defined" in { - clumpResult(Clump.value(1).join(Clump.value(2))) mustEqual Some(1, 2) - } - "one of them is undefined" in { - clumpResult(Clump.value(1).join(Clump.value(None))) mustEqual None + "can be joined with another clump and produce a new clump with the value of both (clump.join)" - { + "both clumps are defined" - { + assertResult(Clump.value(1).join(Clump.value(2)), Some(1, 2)) + } + "one of them is undefined" - { + assertResult(Clump.value(1).join(Clump.value(None)), None) + } } - } - "allows to recover from failures" >> { + "allows to recover from failures" - { - "using a function that recovers using a new value (clump.handle)" >> { - "exception happens" in { - val clump = - Clump.exception(new IllegalStateException).handle { - case e: IllegalStateException => Some(2) + "using a function that recovers using a new value (clump.handle)" - { + "exception happens" - { + val clump = + Clump.exception(new IllegalStateException).handle { + case e: IllegalStateException => Some(2) + } + assertResult(clump, Some(2)) + } + "exception doesn't happen" - { + val clump = + Clump.value(1).handle { + case e: IllegalStateException => None + } + assertResult(clump, Some(1)) + } + "exception isn't caught" - { + val clump = + Clump.exception(new NullPointerException).handle { + case e: IllegalStateException => Some(1) + } + assertFailure[NullPointerException] { + clump } - clumpResult(clump) mustEqual Some(2) + } } - "exception doesn't happen" in { - val clump = - Clump.value(1).handle { - case e: IllegalStateException => None + + "using a function that recovers using a new value (clump.recover)" - { + "exception happens" - { + val clump = + Clump.exception(new IllegalStateException).recover { + case e: IllegalStateException => Some(2) + } + assertResult(clump, Some(2)) + } + "exception doesn't happen" - { + val clump = + Clump.value(1).recover { + case e: IllegalStateException => None + } + assertResult(clump, Some(1)) + } + "exception isn't caught" - { + val clump = + Clump.exception(new NullPointerException).recover { + case e: IllegalStateException => Some(1) + } + assertFailure[NullPointerException] { + clump } - clumpResult(clump) mustEqual Some(1) + } } - "exception isn't caught" in { - val clump = - Clump.exception(new NullPointerException).handle { - case e: IllegalStateException => Some(1) + + "using a function that recovers the failure using a new clump (clump.rescue)" - { + "exception happens" - { + val clump = + Clump.exception(new IllegalStateException).rescue { + case e: IllegalStateException => Clump.value(2) + } + assertResult(clump, Some(2)) + } + "exception doesn't happen" - { + val clump = + Clump.value(1).rescue { + case e: IllegalStateException => Clump.value(None) + } + assertResult(clump, Some(1)) + } + "exception isn't caught" - { + val clump = + Clump.exception(new NullPointerException).rescue { + case e: IllegalStateException => Clump.value(1) + } + assertFailure[NullPointerException] { + clump } - clumpResult(clump) must throwA[NullPointerException] + } } - } - "using a function that recovers using a new value (clump.recover)" >> { - "exception happens" in { - val clump = - Clump.exception(new IllegalStateException).recover { - case e: IllegalStateException => Some(2) + "using a function that recovers the failure using a new clump (clump.recoverWith)" - { + "exception happens" - { + val clump = + Clump.exception(new IllegalStateException).recoverWith { + case e: IllegalStateException => Clump.value(2) + } + assertResult(clump, Some(2)) + } + "exception doesn't happen" - { + val clump = + Clump.value(1).recoverWith { + case e: IllegalStateException => Clump.value(None) + } + assertResult(clump, Some(1)) + } + "exception isn't caught" - { + val clump = + Clump.exception(new NullPointerException).recoverWith { + case e: IllegalStateException => Clump.value(1) + } + assertFailure[NullPointerException] { + clump } - clumpResult(clump) mustEqual Some(2) + } } - "exception doesn't happen" in { - val clump = - Clump.value(1).recover { - case e: IllegalStateException => None - } - clumpResult(clump) mustEqual Some(1) + + "using a function that recovers using a new value (clump.fallback) on any exception" - { + "exception happens" - { + val clump = Clump.exception(new IllegalStateException).fallback(Some(1)) + assertResult(clump, Some(1)) + } + + "exception doesn't happen" - { + val clump = Clump.value(1).fallback(Some(2)) + assertResult(clump, Some(1)) + } } - "exception isn't caught" in { - val clump = - Clump.exception(new NullPointerException).recover { - case e: IllegalStateException => Some(1) - } - clumpResult(clump) must throwA[NullPointerException] + + "using a function that recovers using a new clump (clump.fallbackTo) on any exception" - { + "exception happens" - { + val clump = Clump.exception(new IllegalStateException).fallbackTo(Clump.value(1)) + assertResult(clump, Some(1)) + } + + "exception doesn't happen" - { + val clump = Clump.value(1).fallbackTo(Clump.value(2)) + assertResult(clump, Some(1)) + } } } - "using a function that recovers the failure using a new clump (clump.rescue)" >> { - "exception happens" in { - val clump = - Clump.exception(new IllegalStateException).rescue { - case e: IllegalStateException => Clump.value(2) - } - clumpResult(clump) mustEqual Some(2) - } - "exception doesn't happen" in { - val clump = - Clump.value(1).rescue { - case e: IllegalStateException => Clump.value(None) - } - clumpResult(clump) mustEqual Some(1) - } - "exception isn't caught" in { - val clump = - Clump.exception(new NullPointerException).rescue { - case e: IllegalStateException => Clump.value(1) - } - clumpResult(clump) must throwA[NullPointerException] - } + "can have its result filtered (clump.filter)" - { + assertResult(Clump.value(1).filter(_ != 1), None) + assertResult(Clump.value(1).filter(_ == 1), Some(1)) } - "using a function that recovers the failure using a new clump (clump.recoverWith)" >> { - "exception happens" in { - val clump = - Clump.exception(new IllegalStateException).recoverWith { - case e: IllegalStateException => Clump.value(2) - } - clumpResult(clump) mustEqual Some(2) - } - "exception doesn't happen" in { - val clump = - Clump.value(1).recoverWith { - case e: IllegalStateException => Clump.value(None) - } - clumpResult(clump) mustEqual Some(1) + "uses a covariant type parameter" - { + trait A + class B extends A + class C extends A + val clump: Clump[List[A]] = Clump.traverse(List(new B, new C))(Clump.value(_)) + } + + "allows to defined a fallback value (clump.orElse)" - { + "undefined" - { + assertResult(Clump.empty.orElse(1), Some(1)) } - "exception isn't caught" in { - val clump = - Clump.exception(new NullPointerException).recoverWith { - case e: IllegalStateException => Clump.value(1) - } - clumpResult(clump) must throwA[NullPointerException] + "defined" - { + assertResult(Clump.value(Some(1)).orElse(2), Some(1)) } } - "using a function that recovers using a new value (clump.fallback) on any exception" >> { - "exception happens" in { - val clump = Clump.exception(new IllegalStateException).fallback(Some(1)) - clumpResult(clump) mustEqual Some(1) + "allows to defined a fallback clump (clump.orElse)" - { + "undefined" - { + assertResult(Clump.empty.orElse(Clump.value(1)), Some(1)) } - - "exception doesn't happen" in { - val clump = Clump.value(1).fallback(Some(2)) - clumpResult(clump) mustEqual Some(1) + "defined" - { + assertResult(Clump.value(Some(1)).orElse(Clump.value(2)), Some(1)) } } - "using a function that recovers using a new clump (clump.fallbackTo) on any exception" >> { - "exception happens" in { - val clump = Clump.exception(new IllegalStateException).fallbackTo(Clump.value(1)) - clumpResult(clump) mustEqual Some(1) + "can represent its result as a collection (clump.list) when its type is a collection" - { + "list" - { + Clump.value(List(1, 2)).list.map(result => assert(result == List(1, 2))) } - - "exception doesn't happen" in { - val clump = Clump.value(1).fallbackTo(Clump.value(2)) - clumpResult(clump) mustEqual Some(1) + "set" - { + Clump.value(Set(1, 2)).list.map(result => assert(result == Set(1, 2))) + } + "seq" - { + Clump.value(Seq(1, 2)).list.map(result => assert(result == Seq(1, 2))) + } + "not a collection" - { + compileError("Clump.value(1).flatten") } } - } - - "can have its result filtered (clump.filter)" in { - clumpResult(Clump.value(1).filter(_ != 1)) mustEqual None - clumpResult(Clump.value(1).filter(_ == 1)) mustEqual Some(1) - } - "uses a covariant type parameter" in { - trait A - class B extends A - class C extends A - val clump = Clump.traverse(List(new B, new C))(Clump.value(_)) - (clump: Clump[List[A]]) must beAnInstanceOf[Clump[List[A]]] - } - - "allows to defined a fallback value (clump.orElse)" >> { - "undefined" in { - clumpResult(Clump.empty.orElse(1)) ==== Some(1) - } - "defined" in { - clumpResult(Clump.value(Some(1)).orElse(2)) ==== Some(1) - } - } + "can provide a result falling back to a default (clump.getOrElse)" - { + "initial clump is undefined" - { + Clump.value(None).getOrElse(1).map(result => assert(result == 1)) + } - "allows to defined a fallback clump (clump.orElse)" >> { - "undefined" in { - clumpResult(Clump.empty.orElse(Clump.value(1))) ==== Some(1) - } - "defined" in { - clumpResult(Clump.value(Some(1)).orElse(Clump.value(2))) ==== Some(1) + "initial clump is defined" - { + Clump.value(Some(2)).getOrElse(1).map(result => assert(result == 2)) + } } - } - "can represent its result as a collection (clump.list) when its type is a collection" >> { - "list" in { - awaitResult(Clump.value(List(1, 2)).list) ==== List(1, 2) - } - "set" in { - awaitResult(Clump.value(Set(1, 2)).list) ==== Set(1, 2) - } - "seq" in { - awaitResult(Clump.value(Seq(1, 2)).list) ==== Seq(1, 2) + "has a utility method (clump.apply) for unwrapping optional result" - { + Clump.value(1).apply().map(result => assert(result == 1)) + assertFailure[NoSuchElementException] { + Clump.value[Int](None)() + } } - // Clump.value(1).flatten //doesn't compile - } - "can provide a result falling back to a default (clump.getOrElse)" >> { - "initial clump is undefined" in { - awaitResult(Clump.value(None).getOrElse(1)) ==== 1 - } + "can be made optional (clump.optional) to avoid lossy joins" - { + val clump: Clump[String] = Clump.empty + val optionalClump: Clump[Option[String]] = clump.optional + assertResult(optionalClump, Some(None)) - "initial clump is defined" in { - awaitResult(Clump.value(Some(2)).getOrElse(1)) ==== 2 + val valueClump: Clump[String] = Clump.value("foo") + assertResult(valueClump.join(clump), None) + assertResult(valueClump.join(optionalClump), Some("foo", None)) } } - - "has a utility method (clump.apply) for unwrapping optional result" in { - awaitResult(Clump.value(1).apply()) ==== 1 - awaitResult(Clump.value[Int](None)()) must throwA[NoSuchElementException] - } - - "can be made optional (clump.optional) to avoid lossy joins" in { - val clump: Clump[String] = Clump.empty - val optionalClump: Clump[Option[String]] = clump.optional - clumpResult(optionalClump) ==== Some(None) - - val valueClump: Clump[String] = Clump.value("foo") - clumpResult(valueClump.join(clump)) ==== None - clumpResult(valueClump.join(optionalClump)) ==== Some("foo", None) - } } } diff --git a/src/test/scala/io/getclump/ClumpExecutionSpec.scala b/src/test/scala/io/getclump/ClumpExecutionSpec.scala index 554bc27..1416de5 100644 --- a/src/test/scala/io/getclump/ClumpExecutionSpec.scala +++ b/src/test/scala/io/getclump/ClumpExecutionSpec.scala @@ -1,14 +1,11 @@ package io.getclump +import utest._ import scala.collection.mutable.ListBuffer -import org.junit.runner.RunWith -import org.specs2.specification.Scope -import org.specs2.runner.JUnitRunner -@RunWith(classOf[JUnitRunner]) -class ClumpExecutionSpec extends Spec { +object ClumpExecutionSpec extends Spec { - trait Context extends Scope { + trait Context { val source1Fetches = ListBuffer[Set[Int]]() val source2Fetches = ListBuffer[Set[Int]]() @@ -21,137 +18,150 @@ class ClumpExecutionSpec extends Spec { val source2 = Clump.source((i: Set[Int]) => fetchFunction(source2Fetches, i)) } - "batches requests" >> { + val tests = TestSuite { + "batches requests" - { - "for multiple clumps created from traversed inputs" in new Context { - val clump = - Clump.traverse(List(1, 2, 3, 4)) { - i => - if (i <= 2) - source1.get(i) - else - source2.get(i) + "for multiple clumps created from traversed inputs" - new Context { + val clump = + Clump.traverse(List(1, 2, 3, 4)) { + i => + if (i <= 2) + source1.get(i) + else + source2.get(i) + } + + assertResult(clump, Some(List(10, 20, 30, 40))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3, 4))) } + } - clumpResult(clump) mustEqual Some(List(10, 20, 30, 40)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3, 4)) - } - - "for multiple clumps collected into only one clump" in new Context { - val clump = Clump.collect(source1.get(1), source1.get(2), source2.get(3), source2.get(4)) + "for multiple clumps collected into only one clump" - new Context { + val clump = Clump.collect(source1.get(1), source1.get(2), source2.get(3), source2.get(4)) - clumpResult(clump) mustEqual Some(List(10, 20, 30, 40)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3, 4)) - } + assertResult(clump, Some(List(10, 20, 30, 40))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3, 4))) + } + } - "for clumps created inside nested flatmaps" in new Context { - val clump1 = Clump.value(1).flatMap(source1.get(_)).flatMap(source2.get(_)) - val clump2 = Clump.value(2).flatMap(source1.get(_)).flatMap(source2.get(_)) + "for clumps created inside nested flatmaps" - new Context { + val clump1 = Clump.value(1).flatMap(source1.get(_)).flatMap(source2.get(_)) + val clump2 = Clump.value(2).flatMap(source1.get(_)).flatMap(source2.get(_)) - clumpResult(Clump.collect(clump1, clump2)) mustEqual Some(List(100, 200)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(20, 10)) - } + assertResult(Clump.collect(clump1, clump2), Some(List(100, 200))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(20, 10))) + } + } - "for clumps composed using for comprehension" >> { + "for clumps composed using for comprehension" - { - "one level" in new Context { - val clump = - for { - int <- Clump.collect(source1.get(1), source1.get(2), source2.get(3), source2.get(4)) - } yield int + "one level" - new Context { + val clump = + for { + int <- Clump.collect(source1.get(1), source1.get(2), source2.get(3), source2.get(4)) + } yield int - clumpResult(clump) mustEqual Some(List(10, 20, 30, 40)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3, 4)) - } + assertResult(clump, Some(List(10, 20, 30, 40))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3, 4))) + } + } - "two levels" in new Context { - val clump = - for { - ints1 <- Clump.collect(source1.get(1), source1.get(2)) - ints2 <- Clump.collect(source2.get(3), source2.get(4)) - } yield (ints1, ints2) - - clumpResult(clump) mustEqual Some(List(10, 20), List(30, 40)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3, 4)) - } + "two levels" - new Context { + val clump = + for { + ints1 <- Clump.collect(source1.get(1), source1.get(2)) + ints2 <- Clump.collect(source2.get(3), source2.get(4)) + } yield (ints1, ints2) + + assertResult(clump, Some(List(10, 20), List(30, 40))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3, 4))) + } + } - "with a filter condition" in new Context { - val clump = - for { - ints1 <- Clump.collect(source1.get(1), source1.get(2)) - int2 <- source2.get(3) if (int2 != 999) - } yield (ints1, int2) - - clumpResult(clump) mustEqual Some(List(10, 20), 30) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3)) - } + "with a filter condition" - new Context { + val clump = + for { + ints1 <- Clump.collect(source1.get(1), source1.get(2)) + int2 <- source2.get(3) if (int2 != 999) + } yield (ints1, int2) + + assertResult(clump, Some(List(10, 20), 30)).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3))) + } + } - "using a join" in new Context { - val clump = - for { - ints1 <- Clump.collect(source1.get(1), source1.get(2)) - ints2 <- source2.get(3).join(source2.get(4)) - } yield (ints1, ints2) - - clumpResult(clump) mustEqual Some(List(10, 20), (30, 40)) - source1Fetches mustEqual List(Set(1, 2)) - source2Fetches mustEqual List(Set(3, 4)) - } + "using a join" - new Context { + val clump = + for { + ints1 <- Clump.collect(source1.get(1), source1.get(2)) + ints2 <- source2.get(3).join(source2.get(4)) + } yield (ints1, ints2) + + assertResult(clump, Some(List(10, 20), (30, 40))).map { _ => + assert(source1Fetches == List(Set(1, 2))) + assert(source2Fetches == List(Set(3, 4))) + } + } - "using a future clump as base" in new Context { - val clump = - for { - int <- Clump.future(Future.successful(Some(1))) - collect1 <- Clump.collect(source1.get(int)) - collect2 <- Clump.collect(source2.get(int)) - } yield (collect1, collect2) - - clumpResult(clump) mustEqual Some((List(10), List(10))) - source1Fetches mustEqual List(Set(1)) - source2Fetches mustEqual List(Set(1)) - } + "using a future clump as base" - new Context { + val clump = + for { + int <- Clump.future(Future.successful(Some(1))) + collect1 <- Clump.collect(source1.get(int)) + collect2 <- Clump.collect(source2.get(int)) + } yield (collect1, collect2) + + assertResult(clump, Some((List(10), List(10)))).map { _ => + assert(source1Fetches == List(Set(1))) + assert(source2Fetches == List(Set(1))) + } + } - "complex scenario" in new Context { - val clump = - for { - const1 <- Clump.value(1) - const2 <- Clump.value(2) - collect1 <- Clump.collect(source1.get(const1), source2.get(const2)) - collect2 <- Clump.collect(source1.get(const1), source2.get(const2)) if (true) - (join1a, join1b) <- Clump.value(4).join(Clump.value(5)) - join2 <- source1.get(collect1).join(source2.get(join1b)) - } yield (const1, const2, collect1, collect2, (join1a, join1b), join2) - - clumpResult(clump) mustEqual Some((1, 2, List(10, 20), List(10, 20), (4, 5), (List(100, 200), 50))) - source1Fetches mustEqual List(Set(1), Set(10, 20)) - source2Fetches mustEqual List(Set(2), Set(5)) + "complex scenario" - new Context { + val clump = + for { + const1 <- Clump.value(1) + const2 <- Clump.value(2) + collect1 <- Clump.collect(source1.get(const1), source2.get(const2)) + collect2 <- Clump.collect(source1.get(const1), source2.get(const2)) if (true) + (join1a, join1b) <- Clump.value(4).join(Clump.value(5)) + join2 <- source1.get(collect1).join(source2.get(join1b)) + } yield (const1, const2, collect1, collect2, (join1a, join1b), join2) + + assertResult(clump, Some((1, 2, List(10, 20), List(10, 20), (4, 5), (List(100, 200), 50)))).map { _ => + assert(source1Fetches == List(Set(1), Set(10, 20))) + assert(source2Fetches == List(Set(2), Set(5))) + } + } } } - } - "executes joined clumps in parallel" in new Context { - val promises = List(Promise[Map[Int, Int]](), Promise[Map[Int, Int]]()) + "executes joined clumps - parallel" - new Context { + val promises = List(Promise[Map[Int, Int]](), Promise[Map[Int, Int]]()) - val promisesIterator = promises.iterator + val promisesIterator = promises.iterator - protected override def fetchFunction(fetches: ListBuffer[Set[Int]], inputs: Set[Int]) = - promisesIterator.next.future + protected override def fetchFunction(fetches: ListBuffer[Set[Int]], inputs: Set[Int]) = + promisesIterator.next.future - val clump = source1.get(1).join(source2.get(2)) + val clump = source1.get(1).join(source2.get(2)) - val future: Future[Option[(Int, Int)]] = clump.get + val future: Future[Option[(Int, Int)]] = clump.get - promises.size mustEqual 2 - } + assert(promises.size == 2) + } - "short-circuits the computation in case of a failure" in new Context { - val clump = Clump.exception[Int](new IllegalStateException).map(_ => throw new NullPointerException) - clumpResult(clump) must throwA[IllegalStateException] + "short-circuits the computation - case of a failure" - new Context { + val clump = Clump.exception[Int](new IllegalStateException).map(_ => throw new NullPointerException) + assertFailure[IllegalStateException] { + clump + } + } } } \ No newline at end of file diff --git a/src/test/scala/io/getclump/ClumpFetcherSpec.scala b/src/test/scala/io/getclump/ClumpFetcherSpec.scala index 64a5cfb..ee3a46b 100644 --- a/src/test/scala/io/getclump/ClumpFetcherSpec.scala +++ b/src/test/scala/io/getclump/ClumpFetcherSpec.scala @@ -1,114 +1,107 @@ package io.getclump -import org.junit.runner.RunWith -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.when -import org.specs2.specification.Scope -import org.specs2.runner.JUnitRunner +import utest._ -@RunWith(classOf[JUnitRunner]) -class ClumpFetcherSpec extends Spec { +object ClumpFetcherSpec extends Spec { - trait SetContext extends Scope { + val tests = TestSuite { - trait TestRepository { - def fetch(inputs: Set[Int]): Future[Map[Int, Int]] - } + "memoizes the results of previous fetches" - { + object repo { + def fetch(inputs: List[Int]) = + inputs match { + case List(1, 2) => Future(Map(1 -> 10, 2 -> 20)) + case List(3) => Future(Map(3 -> 30)) + } + } - val repo = smartMock[TestRepository] - } + val source = Clump.source(repo.fetch _) + val clump1 = Clump.traverse(List(1, 2))(source.get) + val clump2 = Clump.traverse(List(2, 3))(source.get) - trait ListContext extends Scope { + val clump = + for { + v1 <- clump1 + v2 <- clump2 + } yield (v1, v2) - trait TestRepository { - def fetch(inputs: List[Int]): Future[List[String]] + assertResult(clump, Some((List(10, 20), List(20, 30)))) } - val repo = smartMock[TestRepository] - } - - "memoizes the results of previous fetches" in new SetContext { - val source = Clump.source(repo.fetch _) - - when(repo.fetch(Set(1, 2))).thenReturn(Future(Map(1 -> 10, 2 -> 20))) - when(repo.fetch(Set(3))).thenReturn(Future(Map(3 -> 30))) + "limits the batch size" - { + object repo { + def fetch(inputs: List[Int]) = + inputs match { + case List(1, 2) => Future(Map(1 -> 10, 2 -> 20)) + case List(3) => Future(Map(3 -> 30)) + } + } - val clump1 = Clump.traverse(List(1, 2))(source.get) - val clump2 = Clump.traverse(List(2, 3))(source.get) + val source = Clump.source(repo.fetch _).maxBatchSize(2) - val clump = - for { - v1 <- clump1 - v2 <- clump2 - } yield (v1, v2) + val clump = Clump.traverse(List(1, 2, 3))(source.get) - clumpResult(clump) mustEqual Some((List(10, 20), List(20, 30))) - - verify(repo).fetch(Set(1, 2)) - verify(repo).fetch(Set(3)) - verifyNoMoreInteractions(repo) - } - - "limits the batch size" in new SetContext { - val source = Clump.source(repo.fetch _).maxBatchSize(2) - - when(repo.fetch(Set(1, 2))).thenReturn(Future(Map(1 -> 10, 2 -> 20))) - when(repo.fetch(Set(3))).thenReturn(Future(Map(3 -> 30))) + assertResult(clump, Some(List(10, 20, 30))) + } - val clump = Clump.traverse(List(1, 2, 3))(source.get) + "retries failed fetches" - { + "success (below the retries limit)" - { + object repo { + private var firstCall = true + def fetch(inputs: List[Int]) = + if (firstCall) { + firstCall = false + Future.failed(new IllegalStateException) + } else + inputs match { + case List(1) => Future(Map(1 -> 10)) + } + } - clumpResult(clump) mustEqual Some(List(10, 20, 30)) + val source = + Clump.source(repo.fetch _).maxRetries { + case e: IllegalStateException => 1 + } - verify(repo).fetch(Set(1, 2)) - verify(repo).fetch(Set(3)) - verifyNoMoreInteractions(repo) - } + assertResult(source.get(1), Some(10)) + } - "retries failed fetches" >> { - "success (below the retries limit)" in new SetContext { - val source = - Clump.source(repo.fetch _).maxRetries { - case e: IllegalStateException => 1 + "failure (above the retries limit)" - { + object repo { + def fetch(inputs: List[Int]): Future[Map[Int, Int]] = + Future.failed(new IllegalStateException) } - when(repo.fetch(Set(1))) - .thenReturn(Future.failed(new IllegalStateException)) - .thenReturn(Future(Map(1 -> 10))) - - clumpResult(source.get(1)) mustEqual Some(10) + val source = + Clump.source(repo.fetch _).maxRetries { + case e: IllegalStateException => 1 + } - verify(repo, times(2)).fetch(Set(1)) - verifyNoMoreInteractions(repo) + assertFailure[IllegalStateException] { + source.get(1) + } + } } - "failure (above the retries limit)" in new SetContext { - val source = - Clump.source(repo.fetch _).maxRetries { - case e: IllegalStateException => 1 + "honours call order for fetches" - { + object repo { + var fetches = List[List[Int]]() + def fetch(inputs: List[Int]) = { + fetches :+= inputs + inputs match { + case List(1, 2, 3) => Future(List("1", "2", "3")) + case List(1, 3, 2) => Future(List("1", "3", "2")) + } } + } + val source = Clump.source(repo.fetch _)(_.toInt) - when(repo.fetch(Set(1))) - .thenReturn(Future.failed(new IllegalStateException)) - .thenReturn(Future.failed(new IllegalStateException)) - - clumpResult(source.get(1)) must throwA[IllegalStateException] - - verify(repo, times(2)).fetch(Set(1)) - verifyNoMoreInteractions(repo) + for { + _ <- Clump.traverse(List(1, 2, 3))(source.get).get + _ <- Clump.traverse(List(1, 3, 2))(source.get).get + } yield { + assert(repo.fetches == List(List(1, 2, 3), List(1, 3, 2))) + } } } - - "honours call order for fetches" in new ListContext { - val source = Clump.source(repo.fetch _)(_.toInt) - when(repo.fetch(List(1, 2, 3))).thenReturn(Future(List("1", "2", "3"))) - when(repo.fetch(List(1, 3, 2))).thenReturn(Future(List("1", "3", "2"))) - - clumpResult(Clump.traverse(List(1, 2, 3))(source.get)) - verify(repo).fetch(List(1, 2, 3)) - - clumpResult(Clump.traverse(List(1, 3, 2))(source.get)) - verify(repo).fetch(List(1, 3, 2)) - } } \ No newline at end of file diff --git a/src/test/scala/io/getclump/ClumpSourceSpec.scala b/src/test/scala/io/getclump/ClumpSourceSpec.scala index f64d55d..64a8ac8 100644 --- a/src/test/scala/io/getclump/ClumpSourceSpec.scala +++ b/src/test/scala/io/getclump/ClumpSourceSpec.scala @@ -1,105 +1,91 @@ package io.getclump -import org.junit.runner.RunWith -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.when -import org.specs2.specification.Scope -import org.specs2.runner.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class ClumpSourceSpec extends Spec { - - trait Context extends Scope { - trait TestRepository { - def fetch(inputs: Set[Int]): Future[Map[Int, Int]] - def fetchWithScope(fromScope: Int, inputs: Set[Int]): Future[Map[Int, Int]] - } - - val repo = smartMock[TestRepository] - } +import utest._ - "fetches an individual clump" in new Context { - val source = Clump.source(repo.fetch _) +object ClumpSourceSpec extends Spec { - when(repo.fetch(Set(1))).thenReturn(Future(Map(1 -> 2))) + val tests = TestSuite { - clumpResult(source.get(1)) mustEqual Some(2) - - verify(repo).fetch(Set(1)) - verifyNoMoreInteractions(repo) - } + "fetches an individual clump" - { + object repo { + def fetch(inputs: List[Int]) = + inputs match { + case List(1) => Future(Map(1 -> 2)) + } + } - "fetches multiple clumps" >> { - - "using list" in new Context { val source = Clump.source(repo.fetch _) - when(repo.fetch(Set(1, 2))).thenReturn(Future(Map(1 -> 10, 2 -> 20))) - - val clump = source.get(List(1, 2)) - - clumpResult(clump) ==== Some(List(10, 20)) - - verify(repo).fetch(Set(1, 2)) - verifyNoMoreInteractions(repo) + assertResult(source.get(1), Some(2)) } - "using set" in new Context { - val source = Clump.source(repo.fetch _) + "fetches multiple clumps" - { - when(repo.fetch(Set(1, 2))).thenReturn(Future(Map(1 -> 10, 2 -> 20))) + "using list" - { + object repo { + def fetch(inputs: List[Int]) = + inputs match { + case List(1, 2) => Future(Map(1 -> 10, 2 -> 20)) + } + } - val clump = source.get(Set(1, 2)) + val source = Clump.source(repo.fetch _) - clumpResult(clump) ==== Some(Set(10, 20)) + val clump = source.get(List(1, 2)) - verify(repo).fetch(Set(1, 2)) - verifyNoMoreInteractions(repo) - } - } + assertResult(clump, Some(List(10, 20))) + } - "can be used as a non-singleton" >> { - "without values from the outer scope" in new Context { - val source = Clump.source(repo.fetch _) + "can be used as a non-singleton" - { + "without values from the outer scope" - { + object repo { + def fetch(inputs: List[Int]) = + inputs match { + case List(1) => Future(Map(1 -> 2)) + } + } + val source = Clump.source(repo.fetch _) - when(repo.fetch(Set(1))).thenReturn(Future(Map(1 -> 2))) + val clump = + Clump.collect { + for (i <- 0 until 5) yield { + source.get(1) + } + } - val clump = - Clump.collect { - (for (i <- 0 until 5) yield { - source.get(List(1)) - }).toList + assertResult(clump, Some(List(2, 2, 2, 2, 2))) } - awaitResult(clump.get) + "with values from the outer scope" - { + val scope = 1 - verify(repo).fetch(Set(1)) - verifyNoMoreInteractions(repo) - } + object repo { + def fetchWithScope(fromScope: Int, inputs: List[Int]) = + (fromScope, inputs) match { + case (scope, List(1)) => Future(Map(1 -> 2)) + } + } - "with values from the outer scope" in new Context { - val scope = 1 - val source = Clump.source((inputs: Set[Int]) => repo.fetchWithScope(scope, inputs)) + val source = Clump.source((inputs: List[Int]) => repo.fetchWithScope(scope, inputs)) - when(repo.fetchWithScope(scope, Set(1))).thenReturn(Future(Map(1 -> 2))) + val clump = + Clump.collect { + for (i <- 0 until 5) yield { + source.get(1) + } + } - val clump = - Clump.collect { - (for (i <- 0 until 5) yield { - source.get(List(1)) - }).toList + assertResult(clump, Some(List(2, 2, 2, 2, 2))) } + } - awaitResult(clump.get) - - verify(repo).fetchWithScope(1, Set(1)) - verifyNoMoreInteractions(repo) + "limits the batch size to 100 by default" - { + object repo { + def fetch(inputs: List[Int]): Future[Map[Int, Int]] = ??? + } + val source = Clump.source(repo.fetch _) + assert(source.maxBatchSize == 100) + } } } - - "limits the batch size to 100 by default" in new Context { - val source = Clump.source(repo.fetch _) - source.maxBatchSize mustEqual 100 - } } \ No newline at end of file diff --git a/src/test/scala/io/getclump/IntegrationSpec.scala b/src/test/scala/io/getclump/IntegrationSpec.scala index 64b756d..119b9e9 100644 --- a/src/test/scala/io/getclump/IntegrationSpec.scala +++ b/src/test/scala/io/getclump/IntegrationSpec.scala @@ -1,10 +1,9 @@ package io.getclump -import org.junit.runner.RunWith -import org.specs2.runner.JUnitRunner +import utest._ + +object IntegrationSpec extends Spec { -@RunWith(classOf[JUnitRunner]) -class IntegrationSpec extends Spec { val tweetRepository = new TweetRepository val userRepository = new UserRepository val zipUserRepository = new ZipUserRepository @@ -22,79 +21,90 @@ class IntegrationSpec extends Spec { val likes = Clump.source(likeRepository.likesFor _)(_.likeId) val tracks = Clump.source(trackRepository.tracksFor _)(_.trackId) - "A Clump should batch calls to services" in { - val tweetRepositoryMock = mock[TweetRepository] - val tweets = Clump.source(tweetRepositoryMock.tweetsFor _) - - val userRepositoryMock = mock[UserRepository] - val users = Clump.source(userRepositoryMock.usersFor _) - val topTracks = Clump.sourceSingle(topTracksRepository.topTracksFor _) - - tweetRepositoryMock.tweetsFor(Set(1L, 2L, 3L)) returns - Future.successful(Map( - 1L -> Tweet("Tweet1", 10), - 2L -> Tweet("Tweet2", 20), - 3L -> Tweet("Tweet3", 30) - )) - - userRepositoryMock.usersFor(Set(10L, 20L, 30L)) returns - Future.successful(Map( - 10L -> User(10, "User10"), - 20L -> User(20, "User20"), - 30L -> User(30, "User30") - )) - - val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get(tweetId) - user <- users.get(tweet.userId) - tracks <- topTracks.get(user) - } yield (tweet, user, tracks) - } + val tests = TestSuite { + + "A Clump should batch calls to services" - { + val tweetRepositoryMock = new TweetRepository { + override def tweetsFor(ids: Set[Long]) = + ids.toList match { + case List(1L, 2L, 3L) => + Future.successful(Map( + 1L -> Tweet("Tweet1", 10), + 2L -> Tweet("Tweet2", 20), + 3L -> Tweet("Tweet3", 30))) + } - awaitResult(enrichedTweets.get) ==== Some(List( - (Tweet("Tweet1", 10), User(10, "User10"), Set(Track(10, "Track10"), Track(11, "Track11"), Track(12, "Track12"))), - (Tweet("Tweet2", 20), User(20, "User20"), Set(Track(20, "Track20"), Track(21, "Track21"), Track(22, "Track22"))), - (Tweet("Tweet3", 30), User(30, "User30"), Set(Track(30, "Track30"), Track(31, "Track31"), Track(32, "Track32"))))) - } + } + val tweets = Clump.source(tweetRepositoryMock.tweetsFor _) + + val userRepositoryMock = new UserRepository { + override def usersFor(ids: Set[Long]) = + ids.toList match { + case List(10L, 20L, 30L) => + Future.successful(Map( + 10L -> User(10, "User10"), + 20L -> User(20, "User20"), + 30L -> User(30, "User30"))) + } + } + val users = Clump.source(userRepositoryMock.usersFor _) + val topTracks = Clump.sourceSingle(topTracksRepository.topTracksFor _) - "A Clump should batch calls to parameterized services" in { - val parameterizedTweetRepositoryMock = mock[ParameterizedTweetRepository] - val tweets = Clump.source(parameterizedTweetRepositoryMock.tweetsFor _) - - val parameterizedUserRepositoryMock = mock[ParameterizedUserRepository] - val users = Clump.source(parameterizedUserRepositoryMock.usersFor _)(_.userId) - - parameterizedTweetRepositoryMock.tweetsFor("foo", Set(1, 2, 3)) returns - Future.successful(Map( - 1L -> Tweet("Tweet1", 10), - 2L -> Tweet("Tweet2", 20), - 3L -> Tweet("Tweet3", 30) - )) - - parameterizedUserRepositoryMock.usersFor("bar", Set(10, 20, 30)) returns - Future.successful(Set( - User(10, "User10"), - User(20, "User20"), - User(30, "User30") - )) - - val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get("foo", tweetId) - user <- users.get("bar", tweet.userId) - } yield (tweet, user) + val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get(tweetId) + user <- users.get(tweet.userId) + tracks <- topTracks.get(user) + } yield (tweet, user, tracks) + } + + assertResult(enrichedTweets, Some(List( + (Tweet("Tweet1", 10), User(10, "User10"), Set(Track(10, "Track10"), Track(11, "Track11"), Track(12, "Track12"))), + (Tweet("Tweet2", 20), User(20, "User20"), Set(Track(20, "Track20"), Track(21, "Track21"), Track(22, "Track22"))), + (Tweet("Tweet3", 30), User(30, "User30"), Set(Track(30, "Track30"), Track(31, "Track31"), Track(32, "Track32")))))) } - awaitResult(enrichedTweets.get) ==== Some(List( - (Tweet("Tweet1", 10), User(10, "User10")), - (Tweet("Tweet2", 20), User(20, "User20")), - (Tweet("Tweet3", 30), User(30, "User30")))) - } + "A Clump should batch calls to parameterized services" - { + val parameterizedTweetRepositoryMock = new ParameterizedTweetRepository { + override def tweetsFor(prefix: String, ids: Set[Long]) = + (prefix, ids.toList) match { + case ("foo", List(1, 2, 3)) => + Future.successful(Map( + 1L -> Tweet("Tweet1", 10), + 2L -> Tweet("Tweet2", 20), + 3L -> Tweet("Tweet3", 30))) + } + } + val tweets = Clump.source(parameterizedTweetRepositoryMock.tweetsFor _) + + val parameterizedUserRepositoryMock = new ParameterizedUserRepository { + override def usersFor(prefix: String, ids: Set[Long]) = + (prefix, ids.toList) match { + case ("bar", List(10, 20, 30)) => + Future.successful(Set( + User(10, "User10"), + User(20, "User20"), + User(30, "User30"))) + } + } + val users = Clump.source(parameterizedUserRepositoryMock.usersFor _)(_.userId) - "it should be able to be used in complex nested fetches" in { - val timelineIds = List(1, 3) - val enrichedTimelines = Clump.traverse(timelineIds) { id => + val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get("foo", tweetId) + user <- users.get("bar", tweet.userId) + } yield (tweet, user) + } + + assertResult(enrichedTweets, Some(List( + (Tweet("Tweet1", 10), User(10, "User10")), + (Tweet("Tweet2", 20), User(20, "User20")), + (Tweet("Tweet3", 30), User(30, "User30"))))) + } + + "it should be able to be used - complex nested fetches" - { + val timelineIds = List(1, 3) + val enrichedTimelines = Clump.traverse(timelineIds) { id => for { timeline <- timelines.get(id) enrichedLikes <- Clump.traverse(timeline.likeIds) { id => @@ -106,145 +116,151 @@ class IntegrationSpec extends Spec { } yield (timeline, enrichedLikes) } - awaitResult(enrichedTimelines.get) ==== Some(List( - (Timeline(1, List(10, 20)), List( - (Like(10, List(100, 200), List(1000, 2000)), List(Track(100, "Track100"), Track(200, "Track200")), List(User(1000, "User1000"), User(2000, "User2000"))), - (Like(20, List(200, 400), List(2000, 4000)), List(Track(200, "Track200"), Track(400, "Track400")), List(User(2000, "User2000"), User(4000, "User4000"))))), - (Timeline(3, List(30, 60)), List( - (Like(30, List(300, 600), List(3000, 6000)), List(Track(300, "Track300"), Track(600, "Track600")), List(User(3000, "User3000"), User(6000, "User6000"))), - (Like(60, List(600, 1200), List(6000, 12000)), List(Track(600, "Track600"), Track(1200, "Track1200")), List(User(6000, "User6000"), User(12000, "User12000"))))))) - } + assertResult(enrichedTimelines, Some(List( + (Timeline(1, List(10, 20)), List( + (Like(10, List(100, 200), List(1000, 2000)), List(Track(100, "Track100"), Track(200, "Track200")), List(User(1000, "User1000"), User(2000, "User2000"))), + (Like(20, List(200, 400), List(2000, 4000)), List(Track(200, "Track200"), Track(400, "Track400")), List(User(2000, "User2000"), User(4000, "User4000"))))), + (Timeline(3, List(30, 60)), List( + (Like(30, List(300, 600), List(3000, 6000)), List(Track(300, "Track300"), Track(600, "Track600")), List(User(3000, "User3000"), User(6000, "User6000"))), + (Like(60, List(600, 1200), List(6000, 12000)), List(Track(600, "Track600"), Track(1200, "Track1200")), List(User(6000, "User6000"), User(12000, "User12000")))))))) + } - "it should be usable with regular maps and flatMaps" in { - val tweetIds = List(1L, 2L, 3L) - val enrichedTweets: Clump[List[(Tweet, User)]] = - Clump.traverse(tweetIds) { tweetId => - tweets.get(tweetId).flatMap(tweet => - users.get(tweet.userId).map(user => (tweet, user))) - } + "it should be usable with regular maps and flatMaps" - { + val tweetIds = List(1L, 2L, 3L) + val enrichedTweets: Clump[List[(Tweet, User)]] = + Clump.traverse(tweetIds) { tweetId => + tweets.get(tweetId).flatMap(tweet => + users.get(tweet.userId).map(user => (tweet, user))) + } + + assertResult(enrichedTweets, Some(List( + (Tweet("Tweet1", 10), User(10, "User10")), + (Tweet("Tweet2", 20), User(20, "User20")), + (Tweet("Tweet3", 30), User(30, "User30"))))) + } - awaitResult(enrichedTweets.get) ==== Some(List( - (Tweet("Tweet1", 10), User(10, "User10")), - (Tweet("Tweet2", 20), User(20, "User20")), - (Tweet("Tweet3", 30), User(30, "User30")))) - } + "it should allow unwrapping Clumped lists with clump.list" - { + val enrichedTweets: Clump[List[(Tweet, User)]] = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get(tweetId) + user <- users.get(tweet.userId) + } yield (tweet, user) + } - "it should allow unwrapping Clumped lists with clump.list" in { - val enrichedTweets: Clump[List[(Tweet, User)]] = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get(tweetId) - user <- users.get(tweet.userId) - } yield (tweet, user) + enrichedTweets.list.map { result => + assert(result == List( + (Tweet("Tweet1", 10), User(10, "User10")), + (Tweet("Tweet2", 20), User(20, "User20")), + (Tweet("Tweet3", 30), User(30, "User30")))) + } } - awaitResult(enrichedTweets.list) ==== List( - (Tweet("Tweet1", 10), User(10, "User10")), - (Tweet("Tweet2", 20), User(20, "User20")), - (Tweet("Tweet3", 30), User(30, "User30"))) - } + "it should work with Clump.sourceZip" - { + val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get(tweetId) + user <- zippedUsers.get(tweet.userId) + } yield (tweet, user) + } - "it should work with Clump.sourceZip" in { - val enrichedTweets = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get(tweetId) - user <- zippedUsers.get(tweet.userId) - } yield (tweet, user) + enrichedTweets.list.map { result => + println("aaa", result) + assert(result == List( + (Tweet("Tweet1", 10), User(10, "User10")), + (Tweet("Tweet2", 20), User(20, "User20")), + (Tweet("Tweet3", 30), User(30, "User30")))) + } } - awaitResult(enrichedTweets.get) ==== Some(List( - (Tweet("Tweet1", 10), User(10, "User10")), - (Tweet("Tweet2", 20), User(20, "User20")), - (Tweet("Tweet3", 30), User(30, "User30")))) - } + "A Clump can have a partial result" - { + val onlyFullObjectGraph: Clump[List[(Tweet, User)]] = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get(tweetId) + user <- filteredUsers.get(tweet.userId) + } yield (tweet, user) + } - "A Clump can have a partial result" in { - val onlyFullObjectGraph: Clump[List[(Tweet, User)]] = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get(tweetId) - user <- filteredUsers.get(tweet.userId) - } yield (tweet, user) - } + assertResult(onlyFullObjectGraph, Some(List((Tweet("Tweet2", 20), User(20, "User20"))))) - awaitResult(onlyFullObjectGraph.get) ==== Some(List((Tweet("Tweet2", 20), User(20, "User20")))) + val partialResponses: Clump[List[(Tweet, Option[User])]] = Clump.traverse(1, 2, 3) { tweetId => + for { + tweet <- tweets.get(tweetId) + user <- filteredUsers.get(tweet.userId).optional + } yield (tweet, user) + } - val partialResponses: Clump[List[(Tweet, Option[User])]] = Clump.traverse(1, 2, 3) { tweetId => - for { - tweet <- tweets.get(tweetId) - user <- filteredUsers.get(tweet.userId).optional - } yield (tweet, user) + assertResult(partialResponses, Some(List( + (Tweet("Tweet1", 10), None), + (Tweet("Tweet2", 20), Some(User(20, "User20"))), + (Tweet("Tweet3", 30), None)))) } - - awaitResult(partialResponses.get) ==== Some(List( - (Tweet("Tweet1", 10), None), - (Tweet("Tweet2", 20), Some(User(20, "User20"))), - (Tweet("Tweet3", 30), None))) } -} -case class Tweet(body: String, userId: Long) + case class Tweet(body: String, userId: Long) -case class User(userId: Long, name: String) + case class User(userId: Long, name: String) -case class Timeline(timelineId: Int, likeIds: List[Long]) + case class Timeline(timelineId: Int, likeIds: List[Long]) -case class Like(likeId: Long, trackIds: List[Long], userIds: List[Long]) + case class Like(likeId: Long, trackIds: List[Long], userIds: List[Long]) -case class Track(trackId: Long, name: String) + case class Track(trackId: Long, name: String) -class TweetRepository { - def tweetsFor(ids: Set[Long]): Future[Map[Long, Tweet]] = { - Future.successful(ids.map(id => id -> Tweet(s"Tweet$id", id * 10)).toMap) + class TweetRepository { + def tweetsFor(ids: Set[Long]): Future[Map[Long, Tweet]] = { + Future.successful(ids.map(id => id -> Tweet(s"Tweet$id", id * 10)).toMap) + } } -} -trait ParameterizedTweetRepository { - def tweetsFor(prefix: String, ids: Set[Long]): Future[Map[Long, Tweet]] -} + trait ParameterizedTweetRepository { + def tweetsFor(prefix: String, ids: Set[Long]): Future[Map[Long, Tweet]] + } -class UserRepository { - def usersFor(ids: Set[Long]): Future[Map[Long, User]] = { - Future.successful(ids.map(id => id -> User(id, s"User$id")).toMap) + class UserRepository { + def usersFor(ids: Set[Long]): Future[Map[Long, User]] = { + Future.successful(ids.map(id => id -> User(id, s"User$id")).toMap) + } } -} -trait ParameterizedUserRepository { - def usersFor(prefix: String, ids: Set[Long]): Future[Set[User]] -} + trait ParameterizedUserRepository { + def usersFor(prefix: String, ids: Set[Long]): Future[Set[User]] + } -class ZipUserRepository { - def usersFor(ids: List[Long]): Future[List[User]] = { - Future.successful(ids.map(id => User(id, s"User$id"))) + class ZipUserRepository { + def usersFor(ids: List[Long]): Future[List[User]] = { + Future.successful(ids.map(id => User(id, s"User$id"))) + } } -} -class FilteredUserRepository { - def usersFor(ids: Set[Long]): Future[Set[User]] = { - Future.successful(ids.filter(_ % 20 == 0).map(id => User(id, s"User$id"))) + class FilteredUserRepository { + def usersFor(ids: Set[Long]): Future[Set[User]] = { + Future.successful(ids.filter(_ % 20 == 0).map(id => User(id, s"User$id"))) + } } -} -class TimelineRepository { - def timelinesFor(ids: Set[Int]): Future[Set[Timeline]] = { - Future.successful(ids.map(id => Timeline(id, List(id * 10, id * 20)))) + class TimelineRepository { + def timelinesFor(ids: Set[Int]): Future[Set[Timeline]] = { + Future.successful(ids.map(id => Timeline(id, List(id * 10, id * 20)))) + } } -} -class LikeRepository { - def likesFor(ids: Set[Long]): Future[Set[Like]] = { - Future.successful(ids.map(id => Like(id, List(id * 10, id * 20), List(id * 100, id * 200)))) + class LikeRepository { + def likesFor(ids: Set[Long]): Future[Set[Like]] = { + Future.successful(ids.map(id => Like(id, List(id * 10, id * 20), List(id * 100, id * 200)))) + } } -} -class TrackRepository { - def tracksFor(ids: Set[Long]): Future[Set[Track]] = { - Future.successful(ids.map(id => Track(id, s"Track$id"))) + class TrackRepository { + def tracksFor(ids: Set[Long]): Future[Set[Track]] = { + Future.successful(ids.map(id => Track(id, s"Track$id"))) + } } -} -class TopTracksRepository { - def topTracksFor(user: User): Future[Set[Track]] = { - def track(id: Long): Track = Track(id, s"Track$id") - val userId = user.userId - Future.successful(Set(track(userId), track(userId + 1), track(userId + 2))) + class TopTracksRepository { + def topTracksFor(user: User): Future[Set[Track]] = { + def track(id: Long): Track = Track(id, s"Track$id") + val userId = user.userId + Future.successful(Set(track(userId), track(userId + 1), track(userId + 2))) + } } } diff --git a/src/test/scala/io/getclump/SourcesSpec.scala b/src/test/scala/io/getclump/SourcesSpec.scala index e6b6a2d..195e322 100644 --- a/src/test/scala/io/getclump/SourcesSpec.scala +++ b/src/test/scala/io/getclump/SourcesSpec.scala @@ -1,197 +1,198 @@ package io.getclump -import org.junit.runner.RunWith -import org.specs2.runner.JUnitRunner +import utest._ -@RunWith(classOf[JUnitRunner]) -class SourcesSpec extends Spec { +object SourcesSpec extends Spec { - "creates a clump source" >> { - "set input" in { - def fetch(inputs: Set[Int]) = Future.successful(inputs.map(i => i -> i.toString).toMap) - val source = Clump.source(fetch _) - clumpResult(source.get(1)) mustEqual Some("1") - } - "list input" in { - def fetch(inputs: List[Int]) = Future.successful(inputs.map(i => i -> i.toString).toMap) - val source = Clump.source(fetch _) - clumpResult(source.get(1)) mustEqual Some("1") - } - "extra params" >> { - "one" in { - def fetch(param1: Int, values: List[Int]) = - Future(values.map(v => v -> v * param1).toMap) - val source = Clump.source(fetch _) - val clump = Clump.collect(source.get(2, 3), source.get(2, 4), source.get(3, 5)) - clumpResult(clump) mustEqual Some(List(6, 8, 15)) - } - "two" in { - def fetch(param1: Int, param2: String, values: List[Int]) = - Future(values.map(v => v -> List(param1, param2, v)).toMap) - val source = Clump.source(fetch _) - val clump = Clump.collect(source.get(1, "2", 3), source.get(1, "2", 4), source.get(2, "3", 5)) - clumpResult(clump) mustEqual Some(List(List(1, "2", 3), List(1, "2", 4), List(2, "3", 5))) - } - "three" in { - def fetch(param1: Int, param2: String, param3: List[String], values: List[Int]) = - Future(values.map(v => v -> List(param1, param2, param3, v)).toMap) + val tests = TestSuite { + + "creates a clump source" - { + "set input" - { + def fetch(inputs: Set[Int]) = Future.successful(inputs.map(i => i -> i.toString).toMap) val source = Clump.source(fetch _) - val clump = Clump.collect(source.get(1, "2", List("a"), 3), source.get(1, "2", List("a"), 4), source.get(2, "3", List("b"), 5)) - clumpResult(clump) mustEqual Some(List(List(1, "2", List("a"), 3), List(1, "2", List("a"), 4), List(2, "3", List("b"), 5))) + assertResult(source.get(1), Some("1")) } - "four" in { - def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, values: List[Int]) = - Future(values.map(v => v -> List(param1, param2, param3, param4, v)).toMap) + "list input" - { + def fetch(inputs: List[Int]) = Future.successful(inputs.map(i => i -> i.toString).toMap) val source = Clump.source(fetch _) - val clump = Clump.collect(source.get(1, "2", List("a"), true, 3), source.get(1, "2", List("a"), true, 4), source.get(2, "3", List("b"), false, 5)) - clumpResult(clump) mustEqual Some(List(List(1, "2", List("a"), true, 3), List(1, "2", List("a"), true, 4), List(2, "3", List("b"), false, 5))) + assertResult(source.get(1), Some("1")) + } + "extra params" - { + "one" - { + def fetch(param1: Int, values: List[Int]) = + Future(values.map(v => v -> v * param1).toMap) + val source = Clump.source(fetch _) + val clump = Clump.collect(source.get(2, 3), source.get(2, 4), source.get(3, 5)) + assertResult(clump, Some(List(6, 8, 15))) + } + "two" - { + def fetch(param1: Int, param2: String, values: List[Int]) = + Future(values.map(v => v -> List(param1, param2, v)).toMap) + val source = Clump.source(fetch _) + val clump = Clump.collect(source.get(1, "2", 3), source.get(1, "2", 4), source.get(2, "3", 5)) + assertResult(clump, Some(List(List(1, "2", 3), List(1, "2", 4), List(2, "3", 5)))) + } + "three" - { + def fetch(param1: Int, param2: String, param3: List[String], values: List[Int]) = + Future(values.map(v => v -> List(param1, param2, param3, v)).toMap) + val source = Clump.source(fetch _) + val clump = Clump.collect(source.get(1, "2", List("a"), 3), source.get(1, "2", List("a"), 4), source.get(2, "3", List("b"), 5)) + assertResult(clump, Some(List(List(1, "2", List("a"), 3), List(1, "2", List("a"), 4), List(2, "3", List("b"), 5)))) + } + "four" - { + def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, values: List[Int]) = + Future(values.map(v => v -> List(param1, param2, param3, param4, v)).toMap) + val source = Clump.source(fetch _) + val clump = Clump.collect(source.get(1, "2", List("a"), true, 3), source.get(1, "2", List("a"), true, 4), source.get(2, "3", List("b"), false, 5)) + assertResult(clump, Some(List(List(1, "2", List("a"), true, 3), List(1, "2", List("a"), true, 4), List(2, "3", List("b"), false, 5)))) + } } } - } - "creates a clump source with key function" >> { - "set input" in { - def fetch(inputs: Set[Int]) = Future.successful(inputs.map(_.toString)) - val source = Clump.source(fetch _)(_.toInt) - clumpResult(source.get(1)) mustEqual Some("1") - } - "seq input" in { - def fetch(inputs: Seq[Int]) = Future.successful(inputs.map(_.toString)) - val source = Clump.source(fetch _)(_.toInt) - clumpResult(source.get(1)) mustEqual Some("1") - } - "extra params" >> { - "one" in { - def fetch(param1: Int, values: List[Int]) = Future(values.map((param1, _))) - val source = Clump.source(fetch _)(_._2) - val clump = Clump.collect(source.get(2, 3), source.get(2, 4), source.get(3, 5)) - clumpResult(clump) mustEqual Some(List((2, 3), (2, 4), (3, 5))) - } - "two" in { - def fetch(param1: Int, param2: String, values: List[Int]) = Future(values.map((param1, param2, _))) - val source = Clump.source(fetch _)(_._3) - val clump = Clump.collect(source.get(1, "2", 3), source.get(1, "2", 4), source.get(2, "3", 5)) - clumpResult(clump) mustEqual Some(List((1, "2", 3), (1, "2", 4), (2, "3", 5))) - } - "three" in { - def fetch(param1: Int, param2: String, param3: List[String], values: List[Int]) = - Future(values.map((param1, param2, param3, _))) - val source = Clump.source(fetch _)(_._4) - val clump = Clump.collect(source.get(1, "2", List("a"), 3), source.get(1, "2", List("a"), 4), source.get(2, "3", List("b"), 5)) - clumpResult(clump) mustEqual Some(List((1, "2", List("a"), 3), (1, "2", List("a"), 4), (2, "3", List("b"), 5))) - } - "four" in { - def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, values: List[Int]) = - Future(values.map((param1, param2, param3, param4, _))) - val source = Clump.source(fetch _)(_._5) - val clump = Clump.collect(source.get(1, "2", List("a"), true, 3), source.get(1, "2", List("a"), true, 4), source.get(2, "3", List("b"), false, 5)) - clumpResult(clump) mustEqual Some(List((1, "2", List("a"), true, 3), (1, "2", List("a"), true, 4), (2, "3", List("b"), false, 5))) + "creates a clump source with key function" - { + "set input" - { + def fetch(inputs: Set[Int]) = Future.successful(inputs.map(_.toString)) + val source = Clump.source(fetch _)(_.toInt) + assertResult(source.get(1), Some("1")) + } + "seq input" - { + def fetch(inputs: Seq[Int]) = Future.successful(inputs.map(_.toString)) + val source = Clump.source(fetch _)(_.toInt) + assertResult(source.get(1), Some("1")) + } + "extra params" - { + "one" - { + def fetch(param1: Int, values: List[Int]) = Future(values.map((param1, _))) + val source = Clump.source(fetch _)(_._2) + val clump = Clump.collect(source.get(2, 3), source.get(2, 4), source.get(3, 5)) + assertResult(clump, Some(List((2, 3), (2, 4), (3, 5)))) + } + "two" - { + def fetch(param1: Int, param2: String, values: List[Int]) = Future(values.map((param1, param2, _))) + val source = Clump.source(fetch _)(_._3) + val clump = Clump.collect(source.get(1, "2", 3), source.get(1, "2", 4), source.get(2, "3", 5)) + assertResult(clump, Some(List((1, "2", 3), (1, "2", 4), (2, "3", 5)))) + } + "three" - { + def fetch(param1: Int, param2: String, param3: List[String], values: List[Int]) = + Future(values.map((param1, param2, param3, _))) + val source = Clump.source(fetch _)(_._4) + val clump = Clump.collect(source.get(1, "2", List("a"), 3), source.get(1, "2", List("a"), 4), source.get(2, "3", List("b"), 5)) + assertResult(clump, Some(List((1, "2", List("a"), 3), (1, "2", List("a"), 4), (2, "3", List("b"), 5)))) + } + "four" - { + def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, values: List[Int]) = + Future(values.map((param1, param2, param3, param4, _))) + val source = Clump.source(fetch _)(_._5) + val clump = Clump.collect(source.get(1, "2", List("a"), true, 3), source.get(1, "2", List("a"), true, 4), source.get(2, "3", List("b"), false, 5)) + assertResult(clump, Some(List((1, "2", List("a"), true, 3), (1, "2", List("a"), true, 4), (2, "3", List("b"), false, 5)))) + } } } - } - "creates a clump source with zip as the key function" >> { - "list input" in { - def fetch(inputs: List[Int]) = Future.successful(inputs.map(_.toString)) - val source = Clump.sourceZip(fetch _) - clumpResult(source.get(1)) mustEqual Some("1") - } - "extra params" >> { - "one" in { - def fetch(param1: Int, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_.toString)) - val source = Clump.sourceZip(fetch _) - clumpResult(source.get(1, 2)) mustEqual Some("3") - } - "two" in { - def fetch(param1: Int, param2: String, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2)) + "creates a clump source with zip as the key function" - { + "list input" - { + def fetch(inputs: List[Int]) = Future.successful(inputs.map(_.toString)) val source = Clump.sourceZip(fetch _) - clumpResult(source.get(1, "a", 2)) mustEqual Some("3a") - } - "three" in { - def fetch(param1: Int, param2: String, param3: List[String], inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2).map(_ + param3.fold("")(_ + _))) - val source = Clump.sourceZip(fetch _) - clumpResult(source.get(1, "a", List("b", "c"), 2)) mustEqual Some("3abc") - } - "four" in { - def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2).map(_ + param3.fold("")(_ + _)).map(_ + s"-$param4")) - val source = Clump.sourceZip(fetch _) - clumpResult(source.get(1, "a", List("b", "c"), true, 2)) mustEqual Some("3abc-true") + assertResult(source.get(1), Some("1")) + } + "extra params" - { + "one" - { + def fetch(param1: Int, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_.toString)) + val source = Clump.sourceZip(fetch _) + assertResult(source.get(1, 2), Some("3")) + } + "two" - { + def fetch(param1: Int, param2: String, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2)) + val source = Clump.sourceZip(fetch _) + assertResult(source.get(1, "a", 2), Some("3a")) + } + "three" - { + def fetch(param1: Int, param2: String, param3: List[String], inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2).map(_ + param3.fold("")(_ + _))) + val source = Clump.sourceZip(fetch _) + assertResult(source.get(1, "a", List("b", "c"), 2), Some("3abc")) + } + "four" - { + def fetch(param1: Int, param2: String, param3: List[String], param4: Boolean, inputs: List[Int]) = Future.successful(inputs.map(_ + param1).map(_ + param2).map(_ + param3.fold("")(_ + _)).map(_ + s"-$param4")) + val source = Clump.sourceZip(fetch _) + assertResult(source.get(1, "a", List("b", "c"), true, 2), Some("3abc-true")) + } } } - } - - "creates a clump source from a singly keyed fetch function" >> { - "single key input" in { - def fetch(input: Int) = Future.successful(Set(input, input + 1, input + 2)) - val source = Clump.sourceSingle(fetch _) + "creates a clump source from a singly keyed fetch function" - { + "single key input" - { + def fetch(input: Int) = Future.successful(Set(input, input + 1, input + 2)) - clumpResult(source.get(1)) ==== Some(Set(1, 2, 3)) - clumpResult(source.get(2)) ==== Some(Set(2, 3, 4)) - clumpResult(source.get(List(1, 2))) ==== Some(List(Set(1, 2, 3), Set(2, 3, 4))) - } - "extra params" >> { - "one" in { - def fetch(param1: String, input: Int) = Future.successful(input + param1) - val source = Clump.sourceSingle(fetch _) - clumpResult(source.get("a", 2)) mustEqual Some("2a") - } - "two" in { - def fetch(param1: String, param2: List[Int], input: Int) = Future.successful(input + param1 + param2.mkString) val source = Clump.sourceSingle(fetch _) - clumpResult(source.get("a", List(1, 2), 3)) mustEqual Some("3a12") - } - "three" in { - def fetch(param1: String, param2: List[Int], param3: Int, input: Int) = Future.successful(input + param1 + param2.mkString + param3) - val source = Clump.sourceSingle(fetch _) - clumpResult(source.get("a", List(1, 2), 3, 4)) mustEqual Some("4a123") - } - "four" in { - def fetch(param1: String, param2: List[Int], param3: Int, param4: List[String], input: Int) = Future.successful(input + param1 + param2.mkString + param3 + param4.mkString) - val source = Clump.sourceSingle(fetch _) - clumpResult(source.get("a", List(1, 2), 3, List("b","c"), 4)) mustEqual Some("4a123bc") + + assertResult(source.get(1), Some(Set(1, 2, 3))) + assertResult(source.get(2), Some(Set(2, 3, 4))) + assertResult(source.get(List(1, 2)), Some(List(Set(1, 2, 3), Set(2, 3, 4)))) + } + "extra params" - { + "one" - { + def fetch(param1: String, input: Int) = Future.successful(input + param1) + val source = Clump.sourceSingle(fetch _) + assertResult(source.get("a", 2), Some("2a")) + } + "two" - { + def fetch(param1: String, param2: List[Int], input: Int) = Future.successful(input + param1 + param2.mkString) + val source = Clump.sourceSingle(fetch _) + assertResult(source.get("a", List(1, 2), 3), Some("3a12")) + } + "three" - { + def fetch(param1: String, param2: List[Int], param3: Int, input: Int) = Future.successful(input + param1 + param2.mkString + param3) + val source = Clump.sourceSingle(fetch _) + assertResult(source.get("a", List(1, 2), 3, 4), Some("4a123")) + } + "four" - { + def fetch(param1: String, param2: List[Int], param3: Int, param4: List[String], input: Int) = Future.successful(input + param1 + param2.mkString + param3 + param4.mkString) + val source = Clump.sourceSingle(fetch _) + assertResult(source.get("a", List(1, 2), 3, List("b", "c"), 4), Some("4a123bc")) + } } } - } - "creates a clump source from various input/ouput type fetch functions (ClumpSource.apply)" in { - def setToSet: Set[Int] => Future[Set[String]] = { inputs => Future.successful(inputs.map(_.toString)) } - def listToList: List[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString)) } - def iterableToIterable: Iterable[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } - def setToList: Set[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } - def listToSet: List[Int] => Future[Set[String]] = { inputs => Future.successful(inputs.map(_.toString).toSet) } - def setToIterable: Set[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } - def listToIterable: List[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } - def iterableToList: Iterable[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } - def iterableToSet: Iterable[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } - - def testSource(source: ClumpSource[Int, String]) = - clumpResult(source.get(List(1, 2))) mustEqual Some(List("1", "2")) - - def extractId(string: String) = string.toInt - - testSource(Clump.source(setToSet)(extractId)) - testSource(Clump.source(listToList)(extractId)) - testSource(Clump.source(iterableToIterable)(extractId)) - testSource(Clump.source(setToList)(extractId)) - testSource(Clump.source(listToSet)(extractId)) - testSource(Clump.source(setToIterable)(extractId)) - testSource(Clump.source(listToIterable)(extractId)) - testSource(Clump.source(iterableToList)(extractId)) - testSource(Clump.source(iterableToSet)(extractId)) - } + "creates a clump source from various input/ouput type fetch functions (ClumpSource.apply)" - { + def setToSet: Set[Int] => Future[Set[String]] = { inputs => Future.successful(inputs.map(_.toString)) } + def listToList: List[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString)) } + def iterableToIterable: Iterable[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } + def setToList: Set[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } + def listToSet: List[Int] => Future[Set[String]] = { inputs => Future.successful(inputs.map(_.toString).toSet) } + def setToIterable: Set[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } + def listToIterable: List[Int] => Future[Iterable[String]] = { inputs => Future.successful(inputs.map(_.toString)) } + def iterableToList: Iterable[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } + def iterableToSet: Iterable[Int] => Future[List[String]] = { inputs => Future.successful(inputs.map(_.toString).toList) } + + def testSource(source: ClumpSource[Int, String]) = + assertResult(source.get(List(1, 2)), Some(List("1", "2"))) + + def extractId(string: String) = string.toInt + + testSource(Clump.source(setToSet)(extractId)) + testSource(Clump.source(listToList)(extractId)) + testSource(Clump.source(iterableToIterable)(extractId)) + testSource(Clump.source(setToList)(extractId)) + testSource(Clump.source(listToSet)(extractId)) + testSource(Clump.source(setToIterable)(extractId)) + testSource(Clump.source(listToIterable)(extractId)) + testSource(Clump.source(iterableToList)(extractId)) + testSource(Clump.source(iterableToSet)(extractId)) + } - "creates a clump source from various input/ouput type fetch functions (ClumpSource.from)" in { + "creates a clump source from various input/ouput type fetch functions (ClumpSource.from)" - { - def setToMap: Set[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } - def listToMap: List[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } - def iterableToMap: Iterable[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } + def setToMap: Set[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } + def listToMap: List[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } + def iterableToMap: Iterable[Int] => Future[Map[Int, String]] = { inputs => Future.successful(inputs.map(input => (input, input.toString)).toMap) } - def testSource(source: ClumpSource[Int, String]) = - clumpResult(source.get(List(1, 2))) mustEqual Some(List("1", "2")) + def testSource(source: ClumpSource[Int, String]) = + assertResult(source.get(List(1, 2)), Some(List("1", "2"))) - testSource(Clump.source(setToMap)) - testSource(Clump.source(listToMap)) - testSource(Clump.source(iterableToMap)) + testSource(Clump.source(setToMap)) + testSource(Clump.source(listToMap)) + testSource(Clump.source(iterableToMap)) + } } } \ No newline at end of file diff --git a/src/test/scala/io/getclump/Spec.scala b/src/test/scala/io/getclump/Spec.scala index 686768f..1335275 100644 --- a/src/test/scala/io/getclump/Spec.scala +++ b/src/test/scala/io/getclump/Spec.scala @@ -1,11 +1,23 @@ package io.getclump -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification -import org.specs2.time.NoTimeConversions +import utest._ +import scala.reflect.ClassTag -trait Spec extends Specification with Mockito with NoTimeConversions { +trait Spec extends TestSuite { - protected def clumpResult[T](clump: Clump[T]) = - awaitResult(clump.get) + object NoError extends Exception + + protected def assertFailure[T: ClassTag](f: Future[_]) = + f.map(_ => throw NoError).recover { + case NoError => throw new IllegalStateException("A failure was expected.") + case e: T => None + } + + protected def assertFailure[T: ClassTag](f: Clump[_]): Future[_] = + assertFailure[T](f.get) + + protected def assertResult[T](clump: Clump[T], expected: T) = + clump.get.map { result => + assert(result == expected) + } }