Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add geospatial commands #164

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(_))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading