diff --git a/core/js-jvm/src/test/scala/io/chrisdavenport/rediculous/RedisCommandsSpec.scala b/core/js-jvm/src/test/scala/io/chrisdavenport/rediculous/RedisCommandsSpec.scala index ce60223..94b795a 100644 --- a/core/js-jvm/src/test/scala/io/chrisdavenport/rediculous/RedisCommandsSpec.scala +++ b/core/js-jvm/src/test/scala/io/chrisdavenport/rediculous/RedisCommandsSpec.scala @@ -285,4 +285,53 @@ class RedisCommandsSpec extends CatsEffectSuite { .assertEquals((1L, 1L)) } } + + test("geospatial"){ + import RedisCommands._ + import GeoUnits.Km + import GeoSearchFrom._ + import GeoSearchBy._ + + redisConnection().flatMap{ connection => + val action = for { + _ <- geoadd[RedisIO]("bikes:rentable", List(((-122.27652, 37.805186), "station:1"))) + _ <- geoadd[RedisIO]("bikes:rentable", List(((-122.2674626, 37.8062344), "station:2"))) + _ <- geoadd[RedisIO]("bikes:rentable", List(((-122.2469854, 37.8104049), "station:3"))) + m <- geosearch[RedisIO]("bikes:rentable", Member("station:1"), Radius(5, Km)) + ll <- geosearch[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Box(5, 5, Km)) + ds <- geosearchwithdist[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Radius(5, Km)) + cs <- geosearchwithcoord[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Radius(5, Km)) + hs <- geosearchwithhash[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Radius(5, Km)) + dcs <- geosearchwithdistcoord[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Radius(5, Km)) + dchs <- geosearchwithdistcoordhash[RedisIO]("bikes:rentable", LonLat(-122.2612767, 37.7936847), Radius(5, Km)) + } yield (m, ll, ds, cs, hs, dcs, dchs) + + action + .run(connection) + .assertEquals(( + List("station:1", "station:2", "station:3"), + List("station:1", "station:2", "station:3"), + List(("station:1", 1.8523), ("station:2", 1.4979), ("station:3", 2.2441)), + List(("station:1", (-122.27652043104172, 37.80518485897756)), ("station:2", (-122.26745992898941, 37.80623423353753)), ("station:3", (-122.24698394536972, 37.81040384984464))), + List(("station:1", 1367952638197536L), ("station:2", 1367952641196278L), ("station:3", 1367953014079341L)), + List(("station:1", 1.8523, (-122.27652043104172, 37.80518485897756)), ("station:2", 1.4979, (-122.26745992898941, 37.80623423353753)), ("station:3", 2.2441, (-122.24698394536972, 37.81040384984464))), + List(("station:1", 1.8523, 1367952638197536L, (-122.27652043104172, 37.80518485897756)), ("station:2", 1.4979, 1367952641196278L, (-122.26745992898941, 37.80623423353753)), ("station:3", 2.2441, 1367953014079341L, (-122.24698394536972, 37.81040384984464))) + )) + } + } + + test("mset"){ + redisConnection().flatMap{ connection => + val action = for { + _ <- RedisCommands.mset[RedisIO](("foo", "1"), ("bar", "2")) + xs <- RedisCommands.mget[RedisIO]("foo", "bar", "baz") + } yield xs + + action.run(connection).assertEquals(List( + "1".some, + "2".some, + None + )) + } + } } diff --git a/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala b/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala index 5493869..e34e7df 100644 --- a/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala +++ b/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala @@ -984,10 +984,7 @@ object RedisCommands { def hsetnx[F[_]: RedisCtx](key: String, field: String, value: String): F[Boolean] = RedisCtx[F].keyed(key, NEL.of("HSETNX", key.encode, field.encode, value.encode)) - private[rediculous] def mset[F[_]: RedisCtx](keyvalue: (String, String)): F[Status] = - RedisCtx[F].keyed(keyvalue._1, NEL("MSET", List(keyvalue._1.encode, keyvalue._2.encode))) - - def mset[F[_]: RedisCtx](keyValue: (String, String), keyValues: (String, String)*): F[List[Status]] = { + def mset[F[_]: RedisCtx](keyValue: (String, String), keyValues: (String, String)*): F[Status] = { val command = NEL("MSET", (keyValue :: keyValues.toList).flatMap(t => List(t._1.encode, t._2.encode))) RedisCtx[F].keyed(keyValue._1, command) } @@ -1151,5 +1148,104 @@ object RedisCommands { def publish[F[_]: RedisCtx](channel: String, message: String): F[Int] = RedisCtx[F].unkeyed[Int](cats.data.NonEmptyList.of("PUBLISH", channel, message)) + final case class GeoAddOpts(condition: Option[Condition], change: Boolean) + object GeoAddOpts { + val default = GeoAddOpts(None, false) + } + + def geoadd[F[_]: RedisCtx](key: String, lonLatMember: List[((Double, Double), String)], options: GeoAddOpts = GeoAddOpts.default): F[Long] = { + val items = lonLatMember.flatMap{ case ((x, y), m) => List(x.encode, y.encode, m.encode)} + val condition = options.condition.toList.map(_.encode) + val change = Alternative[List].guard(options.change).as("CH") + RedisCtx[F].keyed(key, NEL("GEOADD", key :: condition ::: change ::: items)) + } + + sealed trait GeoUnits + object GeoUnits { + case object M extends GeoUnits + case object Km extends GeoUnits + case object Ft extends GeoUnits + case object Mi extends GeoUnits + implicit val arg: RedisArg[GeoUnits] = RedisArg[String].contramap[GeoUnits]{ + case M => "M" + case Km => "KM" + case Ft => "FT" + case Mi => "MI" + } + } + + def geodist[F[_]: RedisCtx](key: String, member1: String, member2: String, units: Option[GeoUnits]): F[Double] = + RedisCtx[F].keyed(key, NEL("GEODIST", List(key, member1, member2) ::: units.map(_.encode).toList)) + + def geohash[F[_]: RedisCtx](key: String, members: List[String]): F[List[String]] = + RedisCtx[F].keyed(key, NEL("GEOHASH", members)) + + def geopos[F[_]: RedisCtx](key: String, members: List[String]): F[List[(Double, Double)]] = + RedisCtx[F].keyed(key, NEL("GEOPOS", members)) + + sealed trait GeoSearchFrom + object GeoSearchFrom { + final case class Member(member: String) extends GeoSearchFrom + final case class LonLat(lon: Double, lat: Double) extends GeoSearchFrom + } + + sealed trait GeoSearchBy + object GeoSearchBy { + final case class Radius(radius: Double, units: GeoUnits) extends GeoSearchBy + final case class Box(width: Double, height: Double, units: GeoUnits) extends GeoSearchBy + } + + sealed trait Sort + object Sort { + case object Asc extends Sort + case object Desc extends Sort + implicit val arg: RedisArg[Sort] = RedisArg[String].contramap[Sort]{ + case Asc => "ASC" + case Desc => "DESC" + } + } + + final case class Count(count: Int, any: Boolean = false) + + final case class GeoSearchOpts(sort: Option[Sort], count: Option[Count]) + object GeoSearchOpts { + val default = GeoSearchOpts(None, None) + } + + def geosearch[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[String]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options))) + + def geosearchwithcoord[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[(String, (Double, Double))]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options) ::: List("WITHCOORD"))) + + def geosearchwithdist[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[(String, Double)]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options) ::: List("WITHDIST"))) + + def geosearchwithhash[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[(String, Long)]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options) ::: List("WITHHASH"))) + + def geosearchwithdistcoord[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[(String, Double, (Double, Double))]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options) ::: List("WITHCOORD", "WITHDIST"))) + + def geosearchwithdistcoordhash[F[_]: RedisCtx](key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts = GeoSearchOpts.default): F[List[(String, Double, Long, (Double, Double))]] = + RedisCtx[F].keyed(key, NEL("GEOSEARCH", geosearchCmd(key, from, by, options) ::: List("WITHCOORD", "WITHDIST", "WITHHASH"))) + + private def geosearchCmd(key: String, from: GeoSearchFrom, by: GeoSearchBy, options: GeoSearchOpts): List[String] = { + val fromEnc = from match { + case GeoSearchFrom.Member(member) => List("FROMMEMBER", member) + case GeoSearchFrom.LonLat(lon, lat) => List("FROMLONLAT", lon.encode, lat.encode) + } + val byEnc = by match { + case GeoSearchBy.Radius(radius, units) => List("BYRADIUS", radius.encode, units.encode) + case GeoSearchBy.Box(width, height, units) => List("BYBOX", width.encode, height.encode, units.encode) + } + val sort = options.sort.map(_.encode).toList + val count = options.count.toList.flatMap { + case Count(count, true) => List("COUNT", count.encode, "ANY") + case Count(count, false) => List("COUNT", count.encode) + } + key :: fromEnc ::: byEnc ::: sort ::: count + } + private def toBV(s: String): ByteVector = ByteVector.encodeUtf8(s).fold(throw _, identity(_)) } diff --git a/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala b/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala index de442a7..9f28397 100644 --- a/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala +++ b/core/shared/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala @@ -122,19 +122,19 @@ object RedisResult extends RedisResultLowPriority{ } } - implicit def kv[K: RedisResult, V: RedisResult]: RedisResult[List[(K, V)]] = - new RedisResult[List[(K, V)]] { - def decode(resp: Resp): Either[Resp,List[(K, V)]] = { + implicit def kv[K: RedisResult]: RedisResult[List[(K, Resp)]] = + new RedisResult[List[(K, Resp)]] { + def decode(resp: Resp): Either[Resp,List[(K, Resp)]] = { - def pairs(l: List[Resp]): Either[Resp,List[(K, V)]] = - Monad[Either[Resp, *]].tailRecM[(List[Resp], List[(K, V)]), List[(K, V)]]((l, Nil)){ + def pairs(l: List[Resp]): Either[Resp,List[(K, Resp)]] = + Monad[Either[Resp, *]].tailRecM[(List[Resp], List[(K, Resp)]), List[(K, Resp)]]((l, Nil)){ case (l, acc) => l match { case Nil => Right(Right(acc)) case _ :: Nil => Left(resp) case x1 :: x2 :: xs => for { k <- RedisResult[K].decode(x1) - v <- RedisResult[V].decode(x2) + v <- RedisResult[Resp].decode(x2) } yield Left((xs, (k, v) :: acc)) } }.map(_.reverse)