diff --git a/build.sbt b/build.sbt index 93bf313d..fb1ba252 100644 --- a/build.sbt +++ b/build.sbt @@ -37,8 +37,8 @@ val commonSettings = Seq( CompilerPlugins.kindProjector, Libraries.catsEffect, Libraries.redisClient, - Libraries.catsLaws % Test, - Libraries.catsTestKit % Test, + Libraries.catsLaws % Test, + Libraries.catsTestKit % Test, Libraries.munitCore % Test, Libraries.munitScalacheck % Test ), @@ -133,8 +133,15 @@ lazy val examples = project .in(file("modules/examples")) .settings(commonSettings: _*) .settings(noPublish) - .settings(libraryDependencies += Libraries.log4CatsSlf4j) - .settings(libraryDependencies += Libraries.logback % "runtime") + .settings( + libraryDependencies ++= Seq( + Libraries.circeCore, + Libraries.circeGeneric, + Libraries.circeParser, + Libraries.log4CatsSlf4j, + Libraries.logback % "runtime" + ) + ) .enablePlugins(AutomateHeaderPlugin) .dependsOn(`redis4cats-log4cats`) .dependsOn(`redis4cats-effects`) 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 afd7ee3b..9969044f 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 @@ -25,8 +25,8 @@ import java.nio.ByteBuffer object Codecs { /** - * Given a base `Fs2RedisCodec[K, K]` and evidence of a split epimorphism between K and V - * a new `Fs2RedisCodec[K, V]` can be derived. + * Given a base RedisCodec[K, K] and a split epimorphism between K and V, + * a new RedisCodec[K, V] can be derived. * */ def derive[K, V]( baseCodec: RedisCodec[K, K], diff --git a/modules/examples/src/main/scala/dev/profunktor/redis4cats/JsonCodecDemo.scala b/modules/examples/src/main/scala/dev/profunktor/redis4cats/JsonCodecDemo.scala new file mode 100644 index 00000000..29bd9398 --- /dev/null +++ b/modules/examples/src/main/scala/dev/profunktor/redis4cats/JsonCodecDemo.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2018-2020 ProfunKtor + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.profunktor.redis4cats + +import cats.effect.{ IO, Resource } +import dev.profunktor.redis4cats.codecs.Codecs +import dev.profunktor.redis4cats.codecs.splits.SplitEpi +import dev.profunktor.redis4cats.data.RedisCodec +import dev.profunktor.redis4cats.effect.Log +import io.circe.generic.auto._ +import io.circe.parser.{ decode => jsonDecode } +import io.circe.syntax._ + +object JsonCodecDemo extends LoggerIOApp { + + import Demo._ + + 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 + } + + def program(implicit log: Log[IO]): IO[Unit] = { + val eventsKey = "events" + + val eventSplitEpi: SplitEpi[String, Event] = + SplitEpi[String, Event]( + str => jsonDecode[Event](str).getOrElse(Event.Unknown), + _.asJson.noSpaces + ) + + val eventsCodec: RedisCodec[String, Event] = + Codecs.derive(RedisCodec.Utf8, eventSplitEpi) + + val commandsApi: Resource[IO, RedisCommands[IO, String, Event]] = + Redis[IO].simple(redisURI, eventsCodec) + + commandsApi + .use { cmd => + for { + x <- cmd.sCard(eventsKey) + _ <- putStrLn(s"Number of events: $x") + _ <- cmd.sAdd(eventsKey, Event.Ack(1), Event.Message(23, "foo")) + y <- cmd.sMembers(eventsKey) + _ <- putStrLn(s"Events: $y") + } yield () + } + } + +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 46e073f3..c9ee6045 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,6 +5,7 @@ object Dependencies { object V { val cats = "2.1.1" val catsEffect = "2.1.3" + val circe = "0.13.0" val fs2 = "2.3.0" val log4cats = "1.1.1" @@ -25,16 +26,21 @@ object Dependencies { val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEffect val fs2Core = "co.fs2" %% "fs2-core" % V.fs2 - val log4CatsCore = log4cats("core") - val log4CatsSlf4j = log4cats("slf4j") + val log4CatsCore = log4cats("core") + + val redisClient = "io.lettuce" % "lettuce-core" % V.lettuce - val redisClient = "io.lettuce" % "lettuce-core" % V.lettuce - val logback = "ch.qos.logback" % "logback-classic" % V.logback + // Examples libraries + val circeCore = "io.circe" %% "circe-core" % V.circe + val circeGeneric = "io.circe" %% "circe-generic" % V.circe + val circeParser = "io.circe" %% "circe-parser" % V.circe + val log4CatsSlf4j = log4cats("slf4j") + val logback = "ch.qos.logback" % "logback-classic" % V.logback // Testing libraries - val catsLaws = cats("core") - val catsTestKit = cats("testkit") - val munitCore = "org.scalameta" %% "munit" % V.munit + val catsLaws = cats("core") + val catsTestKit = cats("testkit") + val munitCore = "org.scalameta" %% "munit" % V.munit val munitScalacheck = "org.scalameta" %% "munit-scalacheck" % V.munit } diff --git a/site/docs/codecs.md b/site/docs/codecs.md new file mode 100644 index 00000000..61436c8c --- /dev/null +++ b/site/docs/codecs.md @@ -0,0 +1,106 @@ +--- +layout: docs +title: "Codecs" +number: 6 +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. + +### Deriving codecs + +Under the `dev.profunktor.redis4cats.codecs.splits._` package, you will find standard `SplitEpi` definitions. + +```scala +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: + +```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) +``` + +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. + +### 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): + +```scala mdoc:silent +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: + +```scala mdoc:silent +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. + +```scala mdoc:silent +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]`. + +```scala mdoc:silent +import cats.effect._ +import dev.profunktor.redis4cats.Redis +import dev.profunktor.redis4cats.effect.Log.NoOp._ +import scala.concurrent.ExecutionContext + +implicit val cs = IO.contextShift(ExecutionContext.global) + +val eventsKey = "events" + +Redis[IO].simple("redis://localhost", eventsCodec) + .use { cmd => + for { + x <- cmd.sCard(eventsKey) + _ <- IO(println(s"Number of events: $x")) + _ <- cmd.sAdd(eventsKey, Event.Ack(1), Event.Message(23, "foo")) + y <- cmd.sMembers(eventsKey) + _ <- IO(println(s"Events: $y")) + } yield () + } +``` + +The full compiling example can be found [here](https://github.com/profunktor/redis4cats/blob/master/modules/examples/src/main/scala/dev/profunktor/redis4cats/JsonCodecDemo.scala). + +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. diff --git a/site/src/main/resources/microsite/data/menu.yml b/site/src/main/resources/microsite/data/menu.yml index e25701be..15b7ef4f 100644 --- a/site/src/main/resources/microsite/data/menu.yml +++ b/site/src/main/resources/microsite/data/menu.yml @@ -68,3 +68,7 @@ options: - title: Pipelining url: pipelining.html menu_section: pipelining + + - title: Codecs + url: codecs.html + menu_section: codecs