Skip to content

Commit

Permalink
Merge pull request #296 from profunktor/feature/compression-encryptio…
Browse files Browse the repository at this point in the history
…n-codecs

Codecs: compression, encryption & docs
  • Loading branch information
gvolpe authored May 15, 2020
2 parents 9a08a9a + 0588cdc commit 917f903
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
)
}
Expand Down
74 changes: 67 additions & 7 deletions modules/core/src/main/scala/dev/profunktor/redis4cats/data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
62 changes: 56 additions & 6 deletions site/docs/codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 917f903

Please sign in to comment.