From 0588cdcf61917c3b791bf98c573185d9e49f0af2 Mon Sep 17 00:00:00 2001
From: Gabriel Volpe <volpegabriel@gmail.com>
Date: Fri, 15 May 2020 13:06:23 +0200
Subject: [PATCH] Codecs: compression, encryption & docs

---
 .../profunktor/redis4cats/codecs/Codecs.scala | 48 ++++++++----
 .../dev/profunktor/redis4cats/data.scala      | 74 +++++++++++++++++--
 .../dev/profunktor/redis4cats/Demo.scala      |  2 +-
 site/docs/codecs.md                           | 62 ++++++++++++++--
 4 files changed, 156 insertions(+), 30 deletions(-)

diff --git a/modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/Codecs.scala b/modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/Codecs.scala
index 9969044f..8d9d6108 100644
--- a/modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/Codecs.scala
+++ b/modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/Codecs.scala
@@ -18,30 +18,46 @@ package dev.profunktor.redis4cats.codecs
 
 import dev.profunktor.redis4cats.codecs.splits.SplitEpi
 import dev.profunktor.redis4cats.data.RedisCodec
-import io.lettuce.core.codec.{ RedisCodec => JRedisCodec, ToByteBufEncoder }
-import io.netty.buffer.ByteBuf
+import io.lettuce.core.codec.{ RedisCodec => JRedisCodec }
 import java.nio.ByteBuffer
 
 object Codecs {
 
   /**
-    * Given a base RedisCodec[K, K] and a split epimorphism between K and V,
-    * a new RedisCodec[K, V] can be derived.
+    * Given a base RedisCodec[K, V1] and a split epimorphism between V1 and V2,
+    * a new RedisCodec[K, V2] can be derived.
     * */
-  def derive[K, V](
-      baseCodec: RedisCodec[K, K],
-      epi: SplitEpi[K, V]
-  ): RedisCodec[K, V] = {
+  def derive[K, V1, V2](
+      baseCodec: RedisCodec[K, V1],
+      epi: SplitEpi[V1, V2]
+  ): RedisCodec[K, V2] = {
     val codec = baseCodec.underlying
     RedisCodec(
-      new JRedisCodec[K, V] with ToByteBufEncoder[K, V] {
-        override def decodeKey(bytes: ByteBuffer): K              = codec.decodeKey(bytes)
-        override def encodeKey(key: K): ByteBuffer                = codec.encodeKey(key)
-        override def encodeValue(value: V): ByteBuffer            = codec.encodeValue(epi.reverseGet(value))
-        override def decodeValue(bytes: ByteBuffer): V            = epi.get(codec.decodeValue(bytes))
-        override def encodeKey(key: K, target: ByteBuf): Unit     = codec.encodeKey(key, target)
-        override def encodeValue(value: V, target: ByteBuf): Unit = codec.encodeValue(epi.reverseGet(value), target)
-        override def estimateSize(keyOrValue: scala.Any): Int     = codec.estimateSize(keyOrValue)
+      new JRedisCodec[K, V2] {
+        override def decodeKey(bytes: ByteBuffer): K    = codec.decodeKey(bytes)
+        override def encodeKey(key: K): ByteBuffer      = codec.encodeKey(key)
+        override def encodeValue(value: V2): ByteBuffer = codec.encodeValue(epi.reverseGet(value))
+        override def decodeValue(bytes: ByteBuffer): V2 = epi.get(codec.decodeValue(bytes))
+      }
+    )
+  }
+
+  /**
+    * Given a base RedisCodec[K1, V1], a split epimorphism between K1 and K2, and
+    * a split epimorphism between V1 and V2, a new RedisCodec[K2, V2] can be derived.
+    * */
+  def derive[K1, K2, V1, V2](
+      baseCodec: RedisCodec[K1, V1],
+      epiKeys: SplitEpi[K1, K2],
+      epiValues: SplitEpi[V1, V2]
+  ): RedisCodec[K2, V2] = {
+    val codec = baseCodec.underlying
+    RedisCodec(
+      new JRedisCodec[K2, V2] {
+        override def decodeKey(bytes: ByteBuffer): K2   = epiKeys.get(codec.decodeKey(bytes))
+        override def encodeKey(key: K2): ByteBuffer     = codec.encodeKey(epiKeys.reverseGet(key))
+        override def encodeValue(value: V2): ByteBuffer = codec.encodeValue(epiValues.reverseGet(value))
+        override def decodeValue(bytes: ByteBuffer): V2 = epiValues.get(codec.decodeValue(bytes))
       }
     )
   }
diff --git a/modules/core/src/main/scala/dev/profunktor/redis4cats/data.scala b/modules/core/src/main/scala/dev/profunktor/redis4cats/data.scala
index f0c41248..c6f4fb9c 100644
--- a/modules/core/src/main/scala/dev/profunktor/redis4cats/data.scala
+++ b/modules/core/src/main/scala/dev/profunktor/redis4cats/data.scala
@@ -16,18 +16,20 @@
 
 package dev.profunktor.redis4cats
 
+import cats.effect.Sync
+import cats.syntax.functor._
+import dev.profunktor.redis4cats.JavaConversions._
 import io.lettuce.core.{ ReadFrom => JReadFrom }
-import io.lettuce.core.codec.{ RedisCodec => JRedisCodec, StringCodec, ToByteBufEncoder }
+import io.lettuce.core.codec.{ ByteArrayCodec, CipherCodec, CompressionCodec, RedisCodec => JRedisCodec, StringCodec }
 import io.lettuce.core.{ KeyScanCursor => JKeyScanCursor }
-import dev.profunktor.redis4cats.JavaConversions._
+import javax.crypto.Cipher
+import javax.crypto.spec.SecretKeySpec
 
 object data {
 
   final case class RedisChannel[K](underlying: K) extends AnyVal
 
-  type JCodec[K, V] = JRedisCodec[K, V] with ToByteBufEncoder[K, V]
-
-  final case class RedisCodec[K, V](underlying: JCodec[K, V]) extends AnyVal
+  final case class RedisCodec[K, V](underlying: JRedisCodec[K, V]) extends AnyVal
   final case class NodeId(value: String) extends AnyVal
 
   final case class KeyScanCursor[K](underlying: JKeyScanCursor[K]) extends AnyVal {
@@ -36,8 +38,66 @@ object data {
   }
 
   object RedisCodec {
-    val Ascii = RedisCodec(StringCodec.ASCII)
-    val Utf8  = RedisCodec(StringCodec.UTF8)
+    val Ascii: RedisCodec[String, String]           = RedisCodec(StringCodec.ASCII)
+    val Utf8: RedisCodec[String, String]            = RedisCodec(StringCodec.UTF8)
+    val Bytes: RedisCodec[Array[Byte], Array[Byte]] = RedisCodec(ByteArrayCodec.INSTANCE)
+
+    /**
+      * It compresses every value sent to Redis and it decompresses every value read
+      * from Redis using the DEFLATE compression algorithm.
+      */
+    def deflate[K, V](codec: RedisCodec[K, V]): RedisCodec[K, V] =
+      RedisCodec(CompressionCodec.valueCompressor(codec.underlying, CompressionCodec.CompressionType.DEFLATE))
+
+    /**
+      * It compresses every value sent to Redis and it decompresses every value read
+      * from Redis using the GZIP compression algorithm.
+      */
+    def gzip[K, V](codec: RedisCodec[K, V]): RedisCodec[K, V] =
+      RedisCodec(CompressionCodec.valueCompressor(codec.underlying, CompressionCodec.CompressionType.GZIP))
+
+    /**
+      * It encrypts every value sent to Redis and it decrypts every value read from
+      * Redis using the supplied CipherSuppliers.
+      */
+    def secure[K, V](
+        codec: RedisCodec[K, V],
+        encrypt: CipherCodec.CipherSupplier,
+        decrypt: CipherCodec.CipherSupplier
+    ): RedisCodec[K, V] =
+      RedisCodec(CipherCodec.forValues(codec.underlying, encrypt, decrypt))
+
+    /**
+      * It creates a CipherSupplier given a secret key for encryption.
+      *
+      * A CipherSupplier is needed for [[RedisCodec.secure]]
+      */
+    def encryptSupplier[F[_]: Sync](key: SecretKeySpec): F[CipherCodec.CipherSupplier] =
+      cipherSupplier[F](key, Cipher.ENCRYPT_MODE)
+
+    /**
+      * It creates a CipherSupplier given a secret key for decryption.
+      *
+      * A CipherSupplier is needed for [[RedisCodec.secure]]
+      */
+    def decryptSupplier[F[_]: Sync](key: SecretKeySpec): F[CipherCodec.CipherSupplier] =
+      cipherSupplier[F](key, Cipher.DECRYPT_MODE)
+
+    private def cipherSupplier[F[_]: Sync](key: SecretKeySpec, mode: Int): F[CipherCodec.CipherSupplier] = {
+      val mkCipher =
+        F.delay {
+          val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+          cipher.init(mode, key)
+          cipher
+        }
+
+      mkCipher.map { cipher =>
+        new CipherCodec.CipherSupplier {
+          override def get(kd: CipherCodec.KeyDescriptor): Cipher = cipher
+        }
+      }
+    }
+
   }
 
   object ReadFrom {
diff --git a/modules/examples/src/main/scala/dev/profunktor/redis4cats/Demo.scala b/modules/examples/src/main/scala/dev/profunktor/redis4cats/Demo.scala
index aadf85a7..0f69944f 100644
--- a/modules/examples/src/main/scala/dev/profunktor/redis4cats/Demo.scala
+++ b/modules/examples/src/main/scala/dev/profunktor/redis4cats/Demo.scala
@@ -26,7 +26,7 @@ object Demo {
   val redisURI: String                        = "redis://localhost"
   val redisClusterURI: String                 = "redis://localhost:30001"
   val stringCodec: RedisCodec[String, String] = RedisCodec.Utf8
-  val longCodec: RedisCodec[String, Long]     = Codecs.derive[String, Long](stringCodec, stringLongEpi)
+  val longCodec: RedisCodec[String, Long]     = Codecs.derive(stringCodec, stringLongEpi)
 
   def putStrLn[A](a: A): IO[Unit] = IO(println(a))
 
diff --git a/site/docs/codecs.md b/site/docs/codecs.md
index 61436c8c..85452951 100644
--- a/site/docs/codecs.md
+++ b/site/docs/codecs.md
@@ -7,11 +7,49 @@ position: 6
 
 # Codecs
 
-Redis is a key-value store, and as such, it is commonly used to store simple values in a "stringy" form. Redis4Cats parameterizes the type of keys and values, allowing you to provide the desired `RedisCodec`. The most common one is `RedisCodec.Utf8` but there's also `RedisCodec.Ascii`, in case you need it.
+Redis is a key-value store, and as such, it is commonly used to store simple values in a "stringy" form. Redis4Cats parameterizes the type of keys and values, allowing you to provide the desired `RedisCodec`. The most common one is `RedisCodec.Utf8` but there's also a `RedisCodec.Ascii` and a `RedisCodec.Bytes` as well.
+
+You can also manipulate existing codecs. The `RedisCodec` object exposes a few functions for this purpose.
+
+### Compression
+
+There are two functions available: `deflate` and `gzip`. Here's an example using the latter:
+
+```scala mdoc:silent
+import dev.profunktor.redis4cats.data.RedisCodec
+
+RedisCodec.gzip(RedisCodec.Utf8)
+```
+
+It manipulates an existing codec to add compression support.
+
+### Encryption
+
+In the same spirit, there's another function `secure`, which takes two extra arguments for encryption and decryption, respectively. These two extra arguments are of type `CipherSupplier`. You can either create your own or use the provided functions, which are effectful.
+
+```scala mdoc:silent
+import cats.effect._
+import javax.crypto.spec.SecretKeySpec
+
+def mkCodec(key: SecretKeySpec): IO[RedisCodec[String, String]] =
+  for {
+    e <- RedisCodec.encryptSupplier[IO](key)
+    d <- RedisCodec.decryptSupplier[IO](key)
+  } yield RedisCodec.secure(RedisCodec.Utf8, e, d)
+```
 
 ### Deriving codecs
 
-Under the `dev.profunktor.redis4cats.codecs.splits._` package, you will find standard `SplitEpi` definitions.
+Redis4Cats defines a `SplitEpi` datatype, which stands for [Split Epimorphism](https://ncatlab.org/nlab/show/split+epimorphism), as explained by Rob Norris at [Scala eXchange 2018](https://skillsmatter.com/skillscasts/11626-keynote-pushing-types-and-gazing-at-the-stars). It sounds more complicated than it actually is. Here's its definition:
+
+```scala
+final case class SplitEpi[A, B](
+    get: A => B,
+    reverseGet: B => A
+) extends (A => B)
+```
+
+Under the `dev.profunktor.redis4cats.codecs.splits._` package, you will find useful `SplitEpi` implementations for codecs.
 
 ```scala
 val stringDoubleEpi: SplitEpi[String, Double] =
@@ -29,13 +67,26 @@ Given a `SplitEpi`, we can derive a `RedisCodec` from an existing one. For examp
 ```scala mdoc:silent
 import dev.profunktor.redis4cats.codecs.Codecs
 import dev.profunktor.redis4cats.codecs.splits._
-import dev.profunktor.redis4cats.data.RedisCodec
 
 val longCodec: RedisCodec[String, Long] =
-  Codecs.derive[String, Long](RedisCodec.Utf8, stringLongEpi)
+  Codecs.derive(RedisCodec.Utf8, stringLongEpi)
 ```
 
-Notice that you can only derive codecs that modify the value type `V` but not the key type `K`. This is because keys are strings 99% of the time. However, you can roll out your own codec if you wish (look at how the implementation of `Codecs.derive`), but that's not recommended.
+This is the most common kind of derivation. That is, the one that operates on the value type `V` since keys are most of the time treated as strings. However, if you wish to derive a codec that also modifies the key type `K`, you can do it by supplying another `SplitEpi` instance for keys.
+
+```scala mdoc:silent
+import dev.profunktor.redis4cats.codecs.Codecs
+import dev.profunktor.redis4cats.codecs.splits._
+import dev.profunktor.redis4cats.data.RedisCodec
+
+case class Keys(value: String)
+
+val keysSplitEpi: SplitEpi[String, Keys] =
+  SplitEpi(Keys.apply, _.value)
+
+val newCodec: RedisCodec[Keys, Long] =
+  Codecs.derive(RedisCodec.Utf8, keysSplitEpi, stringLongEpi)
+```
 
 ### Json codecs
 
@@ -80,7 +131,6 @@ val eventsCodec: RedisCodec[String, Event] =
 Finally, we can put all the pieces together to acquire a `RedisCommands[IO, String, Event]`.
 
 ```scala mdoc:silent
-import cats.effect._
 import dev.profunktor.redis4cats.Redis
 import dev.profunktor.redis4cats.effect.Log.NoOp._
 import scala.concurrent.ExecutionContext