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