Skip to content

Commit 27ea529

Browse files
authored
Merge pull request #159 from 47deg/optional-fetches
Introduce Fetch#optional for performing optional fetches
2 parents 47cc4e9 + 8edd14a commit 27ea529

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

docs/src/main/tut/docs.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,22 @@ object UserSource extends DataSource[UserId, User]{
145145
```
146146

147147
Now that we have a data source we can write a function for fetching users
148-
given an id, we just have to pass a `UserId` as an argument to `Fetch`.
148+
given an id, we just have to pass a `UserId` and the data source as arguments to `Fetch`.
149149

150150
```tut:silent
151151
def getUser[F[_] : ConcurrentEffect](id: UserId): Fetch[F, User] =
152152
Fetch(id, UserSource)
153153
```
154154

155+
### Optional identities
156+
157+
If you want to create a Fetch that doesn't fail if the identity is not found, you can use `Fetch#optional` instead of `Fetch#apply`. Note that instead of a `Fetch[F, A]` you will get a `Fetch[F, Option[A]]`.
158+
159+
```tut:silent
160+
def maybeGetUser[F[_] : ConcurrentEffect](id: UserId): Fetch[F, Option[User]] =
161+
Fetch.optional(id, UserSource)
162+
```
163+
155164
### Data sources that don't support batching
156165

157166
If your data source doesn't support batching, you can simply leave the `batch` method unimplemented. Note that it will use the `fetch` implementation for requesting identities in parallel.

shared/src/main/scala/fetch.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,25 @@ object `package` {
241241
))
242242
)
243243

244+
def optional[F[_] : ConcurrentEffect, I, A](id: I, ds: DataSource[I, A]): Fetch[F, Option[A]] =
245+
Unfetch[F, Option[A]](
246+
for {
247+
deferred <- Deferred[F, FetchStatus]
248+
request = FetchOne(id, ds)
249+
result = deferred.complete _
250+
blocked = BlockedRequest(request, result)
251+
anyDs = ds.asInstanceOf[DataSource[Any, Any]]
252+
blockedRequest = RequestMap(Map(anyDs -> blocked))
253+
} yield Blocked(blockedRequest, Unfetch[F, Option[A]](
254+
deferred.get.map {
255+
case FetchDone(a) =>
256+
Done(Some(a)).asInstanceOf[FetchResult[F, Option[A]]]
257+
case FetchMissing() =>
258+
Done(Option.empty[A])
259+
}
260+
))
261+
)
262+
244263
// Running a Fetch
245264

246265
/**

shared/src/test/scala/FetchTests.scala

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@ package fetch
1818

1919
import org.scalatest.{AsyncFreeSpec, Matchers}
2020

21-
import fetch._
22-
2321
import scala.concurrent._
2422
import scala.concurrent.duration._
2523

2624
import cats._
25+
import cats.temp.par._
2726
import cats.effect._
2827
import cats.instances.list._
28+
import cats.instances.option._
2929
import cats.data.NonEmptyList
30-
import cats.syntax.cartesian._
3130
import cats.syntax.all._
3231

3332
class FetchTests extends AsyncFreeSpec with Matchers {
@@ -717,4 +716,65 @@ class FetchTests extends AsyncFreeSpec with Matchers {
717716
case Left(MissingIdentity(Never(), _, _)) =>
718717
}).unsafeToFuture
719718
}
719+
720+
// Optional fetches
721+
722+
case class MaybeMissing(id: Int)
723+
724+
object MaybeMissingSource extends DataSource[MaybeMissing, Int] {
725+
override def name = "Maybe Missing Source"
726+
727+
override def fetch[F[_]](id: MaybeMissing)(
728+
implicit CF: ConcurrentEffect[F], P: Par[F]
729+
): F[Option[Int]] =
730+
if (id.id % 2 == 0)
731+
Applicative[F].pure(None)
732+
else
733+
Applicative[F].pure(Option(id.id))
734+
}
735+
736+
def maybeOpt[F[_] : ConcurrentEffect](id: Int): Fetch[F, Option[Int]] =
737+
Fetch.optional(MaybeMissing(id), MaybeMissingSource)
738+
739+
"We can run optional fetches" in {
740+
def fetch[F[_] : ConcurrentEffect]: Fetch[F, Option[Int]] =
741+
maybeOpt(1)
742+
743+
Fetch.run[IO](fetch).map(_ shouldEqual Some(1)).unsafeToFuture
744+
}
745+
746+
"We can run optional fetches with traverse" in {
747+
def fetch[F[_] : ConcurrentEffect]: Fetch[F, List[Int]] =
748+
List(1, 2, 3).traverse(maybeOpt[F]).map(_.flatten)
749+
750+
Fetch.run[IO](fetch).map(_ shouldEqual List(1, 3)).unsafeToFuture
751+
}
752+
753+
"We can run optional fetches with other data sources" in {
754+
def fetch[F[_] : ConcurrentEffect]: Fetch[F, List[Int]] = {
755+
val ones = List(1, 2, 3).traverse(one[F])
756+
val maybes = List(1, 2, 3).traverse(maybeOpt[F])
757+
(ones, maybes).mapN { case (os, ms) => os ++ ms.flatten }
758+
}
759+
760+
Fetch.run[IO](fetch).map(_ shouldEqual List(1, 2, 3, 1, 3)).unsafeToFuture
761+
}
762+
763+
"We can make fetches that depend on optional fetch results when they aren't defined" in {
764+
def fetch[F[_] : ConcurrentEffect]: Fetch[F, Int] = for {
765+
maybe <- maybeOpt(2)
766+
result <- maybe.fold(Fetch.pure(42))(i => one(i))
767+
} yield result
768+
769+
Fetch.run[IO](fetch).map(_ shouldEqual 42).unsafeToFuture
770+
}
771+
772+
"We can make fetches that depend on optional fetch results when they are defined" in {
773+
def fetch[F[_] : ConcurrentEffect]: Fetch[F, Int] = for {
774+
maybe <- maybeOpt(1)
775+
result <- maybe.fold(Fetch.pure(42))(i => one(i))
776+
} yield result
777+
778+
Fetch.run[IO](fetch).map(_ shouldEqual 1).unsafeToFuture
779+
}
720780
}

0 commit comments

Comments
 (0)