diff --git a/modules/core/src/main/scala/scalacache/AbstractCache.scala b/modules/core/src/main/scala/scalacache/AbstractCache.scala index a54165e5..7aa39a57 100644 --- a/modules/core/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/src/main/scala/scalacache/AbstractCache.scala @@ -96,26 +96,43 @@ trait AbstractCache[F[_], K, V] extends Cache[F, K, V] with LoggingSupport[F, K] key: K )(ttl: Option[Duration] = None)(f: => V)(implicit flags: Flags): F[V] = cachingF(key)(ttl)(Sync[F].delay(f)) + final override def cachingOption( + key: K + )(ttl: Option[Duration] = None)(f: => Option[V])(implicit flags: Flags): F[Option[V]] = + cachingFOption(key)(ttl)(Sync[F].delay(f)) + override def cachingF( key: K - )(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] = { - checkFlagsAndGet(key) - .handleErrorWith { e => - logger - .ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e)) - .as(None) - } - .flatMap { - case Some(valueFromCache) => F.pure(valueFromCache) - case None => - f.flatTap { calculatedValue => - checkFlagsAndPut(key, calculatedValue, ttl) - .handleErrorWith { e => - logger.ifWarnEnabled { - logger.warn(s"Failed to write to cache. Key = $key", e) - }.void - } - } - } - } + )(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] = + read(key).flatMap { + case Some(valueFromCache) => F.pure(valueFromCache) + case None => f.flatTap(write(key, _, ttl)) + } + + override def cachingFOption( + key: K + )(ttl: Option[Duration] = None)(f: F[Option[V]])(implicit flags: Flags): F[Option[V]] = + read(key).flatMap { + case Some(valueFromCache) => F.pure(Some(valueFromCache)) + case None => + f.flatTap { + case Some(calculatedValue) => write(key, calculatedValue, ttl) + case None => logger.ifDebugEnabled(logger.debug("Calculated value was empty, not writing into cache")).void + } + } + + private def read(key: K)(implicit flags: Flags): F[Option[V]] = + checkFlagsAndGet(key).handleErrorWith { e => + logger + .ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e)) + .as(None) + } + + private def write(key: K, value: V, ttl: Option[Duration])(implicit flags: Flags): F[Unit] = + checkFlagsAndPut(key, value, ttl).handleErrorWith { e => + logger.ifWarnEnabled { + logger.warn(s"Failed to write to cache. Key = $key", e) + }.void + } + } diff --git a/modules/core/src/main/scala/scalacache/Cache.scala b/modules/core/src/main/scala/scalacache/Cache.scala index aa9a497e..72f9b16e 100644 --- a/modules/core/src/main/scala/scalacache/Cache.scala +++ b/modules/core/src/main/scala/scalacache/Cache.scala @@ -80,6 +80,22 @@ trait Cache[F[_], K, V] { */ def caching(key: K)(ttl: Option[Duration])(f: => V)(implicit flags: Flags): F[V] + /** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache (only when it isn't None), + * and return it. + * + * @param key + * The cache key + * @param ttl + * The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. + * @param f + * A block that computes the (optional) value + * @param flags + * Flags used to conditionally alter the behaviour of ScalaCache + * @return + * The value, either retrieved from the cache or computed + */ + def cachingOption(key: K)(ttl: Option[Duration])(f: => Option[V])(implicit flags: Flags): F[Option[V]] + /** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. * * @param key @@ -95,6 +111,22 @@ trait Cache[F[_], K, V] { */ def cachingF(key: K)(ttl: Option[Duration])(f: F[V])(implicit flags: Flags): F[V] + /** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache (only when it isn't None), + * and return it. + * + * @param key + * The cache key + * @param ttl + * The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. + * @param f + * A block that computes the (optional) value + * @param flags + * Flags used to conditionally alter the behaviour of ScalaCache + * @return + * The value, either retrieved from the cache or computed + */ + def cachingFOption(key: K)(ttl: Option[Duration])(f: F[Option[V]])(implicit flags: Flags): F[Option[V]] + /** You should call this when you have finished using this Cache. (e.g. when your application shuts down) * * It will take care of gracefully shutting down the underlying cache client. diff --git a/modules/core/src/test/scala/scalacache/AbstractCacheSpec.scala b/modules/core/src/test/scala/scalacache/AbstractCacheSpec.scala index 75b7ca3e..60ce03f7 100644 --- a/modules/core/src/test/scala/scalacache/AbstractCacheSpec.scala +++ b/modules/core/src/test/scala/scalacache/AbstractCacheSpec.scala @@ -131,6 +131,36 @@ class AbstractCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter { result should be("value from cache") } + it should "run the Option block and cache resulting Some value" in { + var called = false + val result = cache + .cachingOption("myKey")(None) { + called = true + Some("result of block") + } + .unsafeRunSync() + + cache.getCalledWithArgs(0) should be("myKey") + cache.putCalledWithArgs(0) should be(("myKey", "result of block", None)) + called should be(true) + result should be(Some("result of block")) + } + + it should "run the Option block and not cache resulting None value" in { + var called = false + val result = cache + .cachingOption("myKey")(None) { + called = true + None + } + .unsafeRunSync() + + cache.getCalledWithArgs(0) should be("myKey") + cache.putCalledWithArgs should be(empty) + called should be(true) + result should be(None) + } + behavior of "#cachingF (Scala Try mode)" it should "run the block and cache its result with no TTL if the value is not found in the cache" in { @@ -170,6 +200,40 @@ class AbstractCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter { tResult should be("value from cache") } + it should "run the Option block and cache resulting Some value" in { + var called = false + val result = cache + .cachingFOption("myKey")(None) { + SyncIO { + called = true + Some("result of block") + } + } + .unsafeRunSync() + + cache.getCalledWithArgs(0) should be("myKey") + cache.putCalledWithArgs(0) should be(("myKey", "result of block", None)) + called should be(true) + result should be(Some("result of block")) + } + + it should "run the Option block and not cache resulting None value" in { + var called = false + val result = cache + .cachingFOption("myKey")(None) { + SyncIO { + called = true + None + } + } + .unsafeRunSync() + + cache.getCalledWithArgs(0) should be("myKey") + cache.putCalledWithArgs should be(empty) + called should be(true) + result should be(None) + } + behavior of "#caching (sync mode)" it should "run the block and cache its result if the value is not found in the cache" in {