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 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:

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.

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

Redis4Cats defines a SplitEpi datatype, which stands for Split Epimorphism, as explained by Rob Norris at Scala eXchange 2018. It sounds more complicated than it actually is. Here’s its definition:

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.

val stringDoubleEpi: SplitEpi[String, Double] =
  SplitEpi(s => Try(s.toDouble).getOrElse(0), _.toString)

val stringLongEpi: SplitEpi[String, Long] =
  SplitEpi(s => Try(s.toLong).getOrElse(0), _.toString)

val stringIntEpi: SplitEpi[String, Int] =
  SplitEpi(s => Try(s.toInt).getOrElse(0), _.toString)

Given a SplitEpi, we can derive a RedisCodec from an existing one. For example:

import dev.profunktor.redis4cats.codecs.Codecs
import dev.profunktor.redis4cats.codecs.splits._

val longCodec: RedisCodec[String, Long] =
  Codecs.derive(RedisCodec.Utf8, stringLongEpi)

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.

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

In the same way we derived simple codecs, we could have one for Json, in case we are only storing values of a single type. For example, say we have the following algebraic data type (ADT):

sealed trait Event

object Event {
  case class Ack(id: Long) extends Event
  case class Message(id: Long, payload: String) extends Event
  case object Unknown extends Event
}

We can define a SplitEpi[String, Event] that handles the Json encoding and decoding in the following way:

import dev.profunktor.redis4cats.codecs.splits.SplitEpi
import dev.profunktor.redis4cats.effect.Log.NoOp._
import io.circe.generic.auto._
import io.circe.parser.{ decode => jsonDecode }
import io.circe.syntax._

val eventSplitEpi: SplitEpi[String, Event] =
  SplitEpi[String, Event](
    str => jsonDecode[Event](str).getOrElse(Event.Unknown),
    _.asJson.noSpaces
  )

We can then proceed to derive a RedisCodec[String, Event] from an existing one.

import dev.profunktor.redis4cats.codecs.Codecs
import dev.profunktor.redis4cats.data.RedisCodec

val eventsCodec: RedisCodec[String, Event] =
  Codecs.derive(RedisCodec.Utf8, eventSplitEpi)

Finally, we can put all the pieces together to acquire a RedisCommands[IO, String, Event].

import dev.profunktor.redis4cats.Redis
import dev.profunktor.redis4cats.effect.Log.NoOp._

val eventsKey = "events"

Redis[IO].simple("redis://localhost", eventsCodec)
  .use { redis =>
    for {
      x <- redis.sCard(eventsKey)
      _ <- IO(println(s"Number of events: $x"))
      _ <- redis.sAdd(eventsKey, Event.Ack(1), Event.Message(23, "foo"))
      y <- redis.sMembers(eventsKey)
      _ <- IO(println(s"Events: $y"))
    } yield ()
  }

The full compiling example can be found here.

Although it is possible to derive a Json codec in this way, it is mainly preferred to use a simple codec like RedisCodec.Utf8 and manage the encoding / decoding yourself (separation of concerns). In this way, you can have a single active Redis connection for more than one type of message.