Skip to content

Commit cf0036f

Browse files
authored
Merge pull request #565 from ronnnnnnnnnnnnn/feature/355-support-polymorphic-keys
Support polymorphic keys
2 parents 30a9bab + adfa4dd commit cf0036f

File tree

54 files changed

+435
-490
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+435
-490
lines changed

modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ class CaffeineBenchmark {
1717

1818
implicit val clockSyncIO: Clock[SyncIO] = Clock[SyncIO]
1919

20-
val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
21-
implicit val cache: Cache[SyncIO, String] = CaffeineCache[SyncIO, String](underlyingCache)
20+
val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
21+
implicit val cache: Cache[SyncIO, String, String] =
22+
CaffeineCache[SyncIO, String, String](underlyingCache)
2223

2324
val key = "key"
2425
val value: String = "value"

modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object ProfilingMemoize extends App {
1414

1515
implicit val clockSyncIO = Clock[SyncIO]
1616
val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
17-
implicit val cache = CaffeineCache[SyncIO, String](underlyingCache)
17+
implicit val cache = CaffeineCache[SyncIO, String, String](underlyingCache)
1818

1919
val key = "key"
2020
val value: String = "value"
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
11
package scalacache.caffeine
22

3-
import java.time.temporal.ChronoUnit
4-
import java.time.{Instant}
5-
3+
import cats.effect.{Clock, Sync}
4+
import cats.implicits._
65
import com.github.benmanes.caffeine.cache.{Caffeine, Cache => CCache}
7-
import cats.effect.Clock
86
import scalacache.logging.Logger
9-
import scalacache.{AbstractCache, CacheConfig, Entry}
7+
import scalacache.{AbstractCache, Entry}
8+
9+
import java.time.Instant
1010
import scala.concurrent.duration.Duration
1111
import scala.language.higherKinds
12-
import cats.effect.Sync
13-
import java.util.concurrent.TimeUnit
14-
import cats.implicits._
15-
import cats.MonadError
1612

1713
/*
1814
* Thin wrapper around Caffeine.
1915
*
2016
* This cache implementation is synchronous.
2117
*/
22-
class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])(implicit
23-
val config: CacheConfig,
24-
clock: Clock[F]
25-
) extends AbstractCache[F, V] {
18+
class CaffeineCache[F[_]: Sync, K, V](val underlying: CCache[K, Entry[V]])(implicit
19+
val clock: Clock[F]
20+
) extends AbstractCache[F, K, V] {
2621
protected val F: Sync[F] = Sync[F]
2722

2823
override protected final val logger = Logger.getLogger(getClass.getName)
2924

30-
def doGet(key: String): F[Option[V]] = {
25+
def doGet(key: K): F[Option[V]] = {
3126
F.delay {
3227
Option(underlying.getIfPresent(key))
3328
}.flatMap(_.filterA(Entry.isBeforeExpiration[F, V]))
@@ -37,15 +32,15 @@ class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])(imp
3732
}
3833
}
3934

40-
def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] =
35+
def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] =
4136
ttl.traverse(toExpiryTime).flatMap { expiry =>
4237
F.delay {
4338
val entry = Entry(value, expiry)
4439
underlying.put(key, entry)
4540
} *> logCachePut(key, ttl)
4641
}
4742

48-
override def doRemove(key: String): F[Unit] =
43+
override def doRemove(key: K): F[Unit] =
4944
F.delay(underlying.invalidate(key))
5045

5146
override def doRemoveAll: F[Unit] =
@@ -65,17 +60,17 @@ object CaffeineCache {
6560

6661
/** Create a new Caffeine cache.
6762
*/
68-
def apply[F[_]: Sync: Clock, V](implicit config: CacheConfig): F[CaffeineCache[F, V]] =
69-
Sync[F].delay(Caffeine.newBuilder().build[String, Entry[V]]()).map(apply(_))
63+
def apply[F[_]: Sync: Clock, K <: AnyRef, V]: F[CaffeineCache[F, K, V]] =
64+
Sync[F].delay(Caffeine.newBuilder.build[K, Entry[V]]()).map(apply(_))
7065

7166
/** Create a new cache utilizing the given underlying Caffeine cache.
7267
*
7368
* @param underlying
7469
* a Caffeine cache
7570
*/
76-
def apply[F[_]: Sync: Clock, V](
77-
underlying: CCache[String, Entry[V]]
78-
)(implicit config: CacheConfig): CaffeineCache[F, V] =
71+
def apply[F[_]: Sync: Clock, K, V](
72+
underlying: CCache[K, Entry[V]]
73+
): CaffeineCache[F, K, V] =
7974
new CaffeineCache(underlying)
8075

8176
}

modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala

+31-29
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,18 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
3030
unsafeRun(f(ticker)) shouldBe Outcome.succeeded(Some(succeed))
3131
}
3232

33-
private def newCCache = Caffeine.newBuilder.build[String, Entry[String]]
33+
case class MyInt(int: Int)
34+
35+
private def newCCache = Caffeine.newBuilder.build[MyInt, Entry[String]]
3436

3537
private def newFCache[F[_]: Sync, V](
36-
underlying: com.github.benmanes.caffeine.cache.Cache[String, Entry[V]]
38+
underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]]
3739
) = {
38-
CaffeineCache[F, V](underlying)
40+
CaffeineCache[F, MyInt, V](underlying)
3941
}
4042

4143
private def newIOCache[V](
42-
underlying: com.github.benmanes.caffeine.cache.Cache[String, Entry[V]]
44+
underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]]
4345
) = {
4446
newFCache[IO, V](underlying)
4547
}
@@ -49,14 +51,14 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
4951
it should "return the value stored in the underlying cache if expiration is not specified" in ticked { _ =>
5052
val underlying = newCCache
5153
val entry = Entry("hello", expiresAt = None)
52-
underlying.put("key1", entry)
54+
underlying.put(MyInt(1), entry)
5355

54-
newIOCache(underlying).get("key1").map(_ shouldBe Some("hello"))
56+
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello"))
5557
}
5658

5759
it should "return None if the given key does not exist in the underlying cache" in ticked { _ =>
5860
val underlying = newCCache
59-
newIOCache(underlying).get("non-existent key").map(_ shouldBe None)
61+
newIOCache(underlying).get(MyInt(2)).map(_ shouldBe None)
6062
}
6163

6264
it should "return None if the given key exists but the value has expired" in ticked { ticker =>
@@ -65,8 +67,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
6567
val underlying = newCCache
6668
val expiredEntry =
6769
Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).minusSeconds(60)))
68-
underlying.put("key1", expiredEntry)
69-
newIOCache(underlying).get("key1").map(_ shouldBe None)
70+
underlying.put(MyInt(1), expiredEntry)
71+
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe None)
7072
}
7173
}
7274

@@ -76,17 +78,17 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
7678
val underlying = newCCache
7779
val expiredEntry =
7880
Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).plusSeconds(60)))
79-
underlying.put("key1", expiredEntry)
80-
newIOCache(underlying).get("key1").map(_ shouldBe Some("hello"))
81+
underlying.put(MyInt(1), expiredEntry)
82+
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello"))
8183
}
8284
}
8385

8486
behavior of "put"
8587

8688
it should "store the given key-value pair in the underlying cache with no TTL" in ticked { _ =>
8789
val underlying = newCCache
88-
newIOCache(underlying).put("key1")("hello", None) *>
89-
IO { underlying.getIfPresent("key1") }
90+
newIOCache(underlying).put(MyInt(1))("hello", None) *>
91+
IO { underlying.getIfPresent(MyInt(1)) }
9092
.map(_ shouldBe Entry("hello", None))
9193
}
9294

@@ -98,8 +100,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
98100

99101
val underlying = newCCache
100102

101-
newFCache[IO, String](underlying).put("key1")("hello", Some(10.seconds)).map { _ =>
102-
underlying.getIfPresent("key1") should be(Entry("hello", expiresAt = Some(now.plusSeconds(10))))
103+
newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(10.seconds)).map { _ =>
104+
underlying.getIfPresent(MyInt(1)) should be(Entry("hello", expiresAt = Some(now.plusSeconds(10))))
103105
}
104106
}
105107

@@ -108,8 +110,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
108110
val now = Instant.ofEpochMilli(ctx.now().toMillis)
109111

110112
val underlying = newCCache
111-
newFCache[IO, String](underlying).put("key1")("hello", Some(30.days)).map { _ =>
112-
underlying.getIfPresent("key1") should be(
113+
newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(30.days)).map { _ =>
114+
underlying.getIfPresent(MyInt(1)) should be(
113115
Entry("hello", expiresAt = Some(now.plusMillis(30.days.toMillis)))
114116
)
115117
}
@@ -120,20 +122,20 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
120122
it should "delete the given key and its value from the underlying cache" in ticked { _ =>
121123
val underlying = newCCache
122124
val entry = Entry("hello", expiresAt = None)
123-
underlying.put("key1", entry)
124-
underlying.getIfPresent("key1") should be(entry)
125+
underlying.put(MyInt(1), entry)
126+
underlying.getIfPresent(MyInt(1)) should be(entry)
125127

126-
newIOCache(underlying).remove("key1") *>
127-
IO(underlying.getIfPresent("key1")).map(_ shouldBe null)
128+
newIOCache(underlying).remove(MyInt(1)) *>
129+
IO(underlying.getIfPresent(MyInt(1))).map(_ shouldBe null)
128130
}
129131

130132
behavior of "get after put"
131133

132134
it should "store the given key-value pair in the underlying cache with no TTL, then get it back" in ticked { _ =>
133135
val underlying = newCCache
134136
val cache = newIOCache(underlying)
135-
cache.put("key1")("hello", None) *>
136-
cache.get("key1").map { _ shouldBe defined }
137+
cache.put(MyInt(1))("hello", None) *>
138+
cache.get(MyInt(1)).map { _ shouldBe defined }
137139
}
138140

139141
behavior of "get after put with TTL"
@@ -143,28 +145,28 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
143145
val underlying = newCCache
144146
val cache = newFCache[IO, String](underlying)
145147

146-
cache.put("key1")("hello", Some(5.seconds)) *>
147-
cache.get("key1").map { _ shouldBe defined }
148+
cache.put(MyInt(1))("hello", Some(5.seconds)) *>
149+
cache.get(MyInt(1)).map { _ shouldBe defined }
148150
}
149151

150152
it should "store the given key-value pair with the given TTL, then get it back (after a sleep) when not expired" in ticked {
151153
implicit ticker =>
152154
val underlying = newCCache
153155
val cache = newFCache[IO, String](underlying)
154156

155-
cache.put("key1")("hello", Some(50.seconds)) *>
157+
cache.put(MyInt(1))("hello", Some(50.seconds)) *>
156158
IO.sleep(40.seconds) *> // sleep, but not long enough for the entry to expire
157-
cache.get("key1").map { _ shouldBe defined }
159+
cache.get(MyInt(1)).map { _ shouldBe defined }
158160
}
159161

160162
it should "store the given key-value pair with the given TTL, then return None if the entry has expired" in ticked {
161163
implicit ticker =>
162164
val underlying = newCCache
163165
val cache = newFCache[IO, String](underlying)
164166

165-
cache.put("key1")("hello", Some(50.seconds)) *>
167+
cache.put(MyInt(1))("hello", Some(50.seconds)) *>
166168
IO.sleep(60.seconds) *> // sleep long enough for the entry to expire
167-
cache.get("key1").map { _ shouldBe empty }
169+
cache.get(MyInt(1)).map { _ shouldBe empty }
168170
}
169171

170172
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package scalacache.serialization
22

33
import java.nio.ByteBuffer
4-
54
import io.circe.jawn.JawnParser
6-
import io.circe.{Decoder, Encoder}
5+
import scalacache.serialization.binary.BinaryCodec
76

87
package object circe {
98

109
private[this] val parser = new JawnParser
1110

12-
implicit def codec[A](implicit encoder: Encoder[A], decoder: Decoder[A]): Codec[A] = new Codec[A] {
11+
implicit def codec[A](implicit encoder: io.circe.Encoder[A], decoder: io.circe.Decoder[A]): BinaryCodec[A] =
12+
new BinaryCodec[A] {
1313

14-
override def encode(value: A): Array[Byte] = encoder.apply(value).noSpaces.getBytes("utf-8")
14+
override def encode(value: A): Array[Byte] = encoder.apply(value).noSpaces.getBytes("utf-8")
1515

16-
override def decode(bytes: Array[Byte]): Codec.DecodingResult[A] =
17-
parser.decodeByteBuffer(ByteBuffer.wrap(bytes)).left.map(FailedToDecode)
16+
override def decode(bytes: Array[Byte]): Codec.DecodingResult[A] =
17+
parser.decodeByteBuffer(ByteBuffer.wrap(bytes)).left.map(FailedToDecode)
1818

19-
}
19+
}
2020

2121
}

modules/circe/src/test/scala/scalacache/serialization/CirceCodecSpec.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.circe.syntax._
66
import org.scalatest.flatspec.AnyFlatSpec
77
import org.scalatest.matchers.should.Matchers
88
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
9+
import scalacache.serialization.binary.BinaryCodec
910

1011
case class Fruit(name: String, tastinessQuotient: Double)
1112

@@ -15,7 +16,7 @@ class CirceCodecSpec extends AnyFlatSpec with Matchers with ScalaCheckDrivenProp
1516

1617
import scalacache.serialization.circe._
1718

18-
private def serdesCheck[A: Arbitrary](expectedJson: A => String)(implicit codec: Codec[A]): Unit = {
19+
private def serdesCheck[A: Arbitrary](expectedJson: A => String)(implicit codec: BinaryCodec[A]): Unit = {
1920
forAll(minSuccessful(10000)) { (a: A) =>
2021
val serialised = codec.encode(a)
2122
new String(serialised, "utf-8") shouldBe expectedJson(a)
@@ -62,7 +63,7 @@ class CirceCodecSpec extends AnyFlatSpec with Matchers with ScalaCheckDrivenProp
6263

6364
it should "serialize and deserialize a case class" in {
6465
import io.circe.generic.auto._
65-
val fruitCodec = implicitly[Codec[Fruit]]
66+
val fruitCodec = implicitly[BinaryCodec[Fruit]]
6667

6768
val banana = Fruit("banana", 0.7)
6869
val serialised = fruitCodec.encode(banana)

modules/core/src/main/scala-2/scalacache/memoization/Macros.scala

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
package scalacache.memoization
22

3-
import scala.language.experimental.macros
4-
import scala.reflect.macros.blackbox
3+
import scalacache.{Cache, Flags}
4+
55
import scala.concurrent.duration.Duration
6+
import scala.language.experimental.macros
67
import scala.language.higherKinds
7-
import scalacache.{Flags, Cache}
8+
import scala.reflect.macros.blackbox
89

910
class Macros(val c: blackbox.Context) {
1011
import c.universe._
1112

1213
def memoizeImpl[F[_], V: c.WeakTypeTag](
1314
ttl: c.Expr[Option[Duration]]
14-
)(f: c.Tree)(cache: c.Expr[Cache[F, V]], flags: c.Expr[Flags]): c.Tree = {
15+
)(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = {
1516
commonMacroImpl(
16-
cache,
17+
config,
1718
{ keyName =>
18-
q"""$cache.cachingForMemoize($keyName)($ttl)($f)($flags)"""
19+
q"""$cache.caching($keyName)($ttl)($f)($flags)"""
1920
}
2021
)
2122
}
2223

2324
def memoizeFImpl[F[_], V: c.WeakTypeTag](
2425
ttl: c.Expr[Option[Duration]]
25-
)(f: c.Tree)(cache: c.Expr[Cache[F, V]], flags: c.Expr[Flags]): c.Tree = {
26+
)(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = {
2627
commonMacroImpl(
27-
cache,
28+
config,
2829
{ keyName =>
29-
q"""$cache.cachingForMemoizeF($keyName)($ttl)($f)($flags)"""
30+
q"""$cache.cachingF($keyName)($ttl)($f)($flags)"""
3031
}
3132
)
3233
}
3334

3435
private def commonMacroImpl[F[_], V: c.WeakTypeTag](
35-
cache: c.Expr[Cache[F, V]],
36+
config: c.Expr[MemoizationConfig],
3637
keyNameToCachingCall: (c.TermName) => c.Tree
3738
): Tree = {
3839

@@ -52,7 +53,7 @@ class Macros(val c: blackbox.Context) {
5253
val keyName = createKeyName()
5354
val cachingCall = keyNameToCachingCall(keyName)
5455
val tree = q"""
55-
val $keyName = $cache.config.memoization.toStringConverter.toString($classNameTree, $classParamssTree, $methodNameTree, $methodParamssTree)
56+
val $keyName = $config.toStringConverter.toString($classNameTree, $classParamssTree, $methodNameTree, $methodParamssTree)
5657
$cachingCall
5758
"""
5859
// println(showCode(tree))

modules/core/src/main/scala-2/scalacache/memoization/package.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ package object memoization {
3434
* @return
3535
* A result, either retrieved from the cache or calculated by executing the function `f`
3636
*/
37-
def memoize[F[_], V](ttl: Option[Duration])(f: => V)(implicit cache: Cache[F, V], flags: Flags): F[V] =
37+
def memoize[F[_], V](ttl: Option[Duration])(
38+
f: => V
39+
)(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] =
3840
macro Macros.memoizeImpl[F, V]
3941

4042
/** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the
@@ -64,6 +66,6 @@ package object memoization {
6466
*/
6567
def memoizeF[F[_], V](
6668
ttl: Option[Duration]
67-
)(f: F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] =
69+
)(f: F[V])(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] =
6870
macro Macros.memoizeFImpl[F, V]
6971
}

0 commit comments

Comments
 (0)