diff --git a/modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/json.scala b/modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/json.scala new file mode 100644 index 00000000..410fdd72 --- /dev/null +++ b/modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/json.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2018-2021 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.algebra + +import dev.profunktor.redis4cats.algebra.json.JsonGetArgs +import io.lettuce.core.json.arguments.{ JsonRangeArgs, JsonGetArgs => LJsonGetArgs } +import io.lettuce.core.json.{ JsonPath, JsonType, JsonValue } + +trait JsonCommands[F[_], K, V] + extends JsonArray[F, K, V] + with JsonGet[F, K, V] + with JsonSet[F, K, V] + with JsonNumber[F, K, V] + with JsonString[F, K, V] + with JsonBoolean[F, K, V] { + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + */ + def clear(key: K, path: JsonPath): F[Long] + def clear(key: K): F[Long] + + /** + * Deletes a value inside the JSON document at a given JsonPath + * @return Long the number of values removed (0 or more). + */ + def del(key: K, path: JsonPath): F[Long] + def del(key: K): F[Long] + + def jsonType(key: K, path: JsonPath): F[List[JsonType]] + def jsonType(key: K): F[List[JsonType]] +} +trait JsonGet[F[_], K, V] { + def get(key: K, path: JsonPath, paths: JsonPath*): F[List[JsonValue]] + def get(key: K, arg: JsonGetArgs, path: JsonPath, paths: JsonPath*): F[List[JsonValue]] + def mget(path: JsonPath, key: K, keys: K*): F[List[JsonValue]] + def objKeys(key: K, path: JsonPath): F[List[V]] + def objLen(key: K, path: JsonPath): F[Long] +} +trait JsonSet[F[_], K, V] { + def mset(key: K, values: (JsonPath, JsonValue)*): F[Boolean] + def set(key: K, path: JsonPath, value: JsonValue): F[Boolean] + def setnx(key: K, path: JsonPath, value: JsonValue): F[Boolean] + def setxx(key: K, path: JsonPath, value: JsonValue): F[Boolean] + def jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): F[String]; +} +trait JsonNumber[F[_], K, V] { + def numIncrBy(key: K, path: JsonPath, number: Number): F[List[Number]] +} +trait JsonString[F[_], K, V] { + def strAppend(key: K, path: JsonPath, value: JsonValue): F[List[Long]] + def strLen(key: K, path: JsonPath): F[Long] +} +trait JsonBoolean[F[_], K, V] { + def toggle(key: K, path: JsonPath): F[List[Long]] +} + +trait JsonArray[F[_], K, V] { + def arrAppend(key: K, path: JsonPath, value: JsonValue*): F[Unit] + def arrIndex(key: K, path: JsonPath, value: JsonValue, range: JsonRangeArgs): F[List[Long]] + def arrInsert(key: K, path: JsonPath, index: Int, value: JsonValue*): F[List[Long]] + def arrLen(key: K, path: JsonPath): F[List[Long]] + def arrPop(key: K, path: JsonPath, index: Int): F[List[JsonValue]] + def arrTrim(key: K, path: JsonPath, range: JsonRangeArgs): F[List[Long]] + +} + +object json { + final case class JsonGetArgs( + indent: Option[String], + newline: Option[String], + space: Option[String] + ) { + def underlying: LJsonGetArgs = { + val args = new LJsonGetArgs() + indent.foreach(args.indent) + newline.foreach(args.newline) + space.foreach(args.space) + args + } + } + + object JsonGetArgs +} diff --git a/modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala b/modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala index 727855d3..5ecbfacf 100644 --- a/modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala +++ b/modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala @@ -22,6 +22,7 @@ import dev.profunktor.redis4cats.effect.Log trait RedisCommands[F[_], K, V] extends StringCommands[F, K, V] + with JsonCommands[F, K, V] with HashCommands[F, K, V] with SetCommands[F, K, V] with SortedSetCommands[F, K, V] diff --git a/modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala b/modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala index 8172958a..ab84a44b 100644 --- a/modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala +++ b/modules/effects/src/main/scala/dev/profunktor/redis4cats/redis.scala @@ -20,7 +20,7 @@ import cats._ import cats.data.NonEmptyList import cats.effect.kernel._ import cats.syntax.all._ -import dev.profunktor.redis4cats.algebra.BitCommandOperation +import dev.profunktor.redis4cats.algebra.{ json, BitCommandOperation } import dev.profunktor.redis4cats.algebra.BitCommandOperation.Overflows import dev.profunktor.redis4cats.config.Redis4CatsConfig import dev.profunktor.redis4cats.connection._ @@ -32,6 +32,8 @@ import dev.profunktor.redis4cats.tx.{ TransactionDiscarded, TxRunner, TxStore } import io.lettuce.core.api.async.RedisAsyncCommands import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands import io.lettuce.core.cluster.api.sync.{ RedisClusterCommands => RedisClusterSyncCommands } +import io.lettuce.core.json.arguments.{ JsonMsetArgs, JsonRangeArgs, JsonSetArgs } +import io.lettuce.core.json.{ JsonPath, JsonType, JsonValue } import io.lettuce.core.{ BitFieldArgs, ClientOptions, @@ -43,11 +45,11 @@ import io.lettuce.core.{ ZAddArgs, ZAggregateArgs, ZStoreArgs, + CopyArgs => JCopyArgs, ExpireArgs => JExpireArgs, FlushMode => JFlushMode, FunctionRestoreMode => JFunctionRestoreMode, GetExArgs => JGetExArgs, - CopyArgs => JCopyArgs, Limit => JLimit, Range => JRange, ReadFrom => JReadFrom, @@ -58,6 +60,7 @@ import io.lettuce.core.{ import org.typelevel.keypool.KeyPool import java.time.Instant +import java.util import java.util.concurrent.TimeUnit import scala.concurrent.duration._ @@ -686,7 +689,7 @@ private[redis4cats] class BaseRedis[F[_]: FutureLift: MonadThrow: Log, K, V]( case SetArg.Ttl.Keep => jSetArgs.keepttl() } - async.flatMap(_.set(key, value, jSetArgs).futureLift.map(_ == "OK")) + async.flatMap(_.set(key, value, jSetArgs).futureLift.map(_.isSuccess)) } override def setNx(key: K, value: V): F[Boolean] = @@ -752,6 +755,98 @@ private[redis4cats] class BaseRedis[F[_]: FutureLift: MonadThrow: Log, K, V]( override def mSetNx(keyValues: Map[K, V]): F[Boolean] = async.flatMap(_.msetnx(keyValues.asJava).futureLift.map(x => Boolean.box(x))) + /** ***************************** JSON API ********************************* */ + override def jsonType(key: K, path: JsonPath): F[List[JsonType]] = + async.flatMap(_.jsonType(key, path).futureLift.map(_.asScala.toList)) + + override def jsonType(key: K): F[List[JsonType]] = async.flatMap(_.jsonType(key).futureLift.map(_.asScala.toList)) + + override def clear(key: K, path: JsonPath): F[Long] = + async.flatMap(_.jsonClear(key, path).futureLift.map(x => Long.box(x))) + + override def clear(key: K): F[Long] = + async.flatMap(_.jsonClear(key).futureLift.map(x => Long.box(x))) + + override def del(key: K, path: JsonPath): F[Long] = + async.flatMap(_.jsonDel(key, path).futureLift.map(x => Long.box(x))) + + override def del(key: K): F[Long] = async.flatMap(_.jsonDel(key).futureLift.map(x => Long.box(x))) + + /*** JSON GETTERS ***/ + override def get(key: K, path: JsonPath, paths: JsonPath*): F[List[JsonValue]] = { + val all = path +: paths + async.flatMap(_.jsonGet(key, all: _*).futureLift.map(_.asScala.toList)) + } + + override def get(key: K, arg: json.JsonGetArgs, path: JsonPath, paths: JsonPath*): F[List[JsonValue]] = { + val all = path +: paths + val options = arg.underlying + async.flatMap(_.jsonGet(key, options, all: _*).futureLift.map(_.asScala.toList)) + } + + override def mget(path: JsonPath, key: K, keys: K*): F[List[JsonValue]] = { + val all = key +: keys + async.flatMap(_.jsonMGet(path, all: _*).futureLift.map(_.asScala.toList)) + } + + override def objKeys(key: K, path: JsonPath): F[List[V]] = + async.flatMap(_.jsonObjkeys(key, path).futureLift.map(_.asScala.toList)) + + override def objLen(key: K, path: JsonPath): F[Long] = + async.flatMap(_.jsonObjlen(key, path).futureLift.map(x => Long.unbox(x))) + + /*** JSON ARRAY ***/ + override def arrAppend(key: K, path: JsonPath, value: JsonValue*): F[Unit] = + async.flatMap(_.jsonArrappend(key, path, value: _*).futureLift.void) + + override def arrIndex(key: K, path: JsonPath, value: JsonValue, range: JsonRangeArgs): F[List[Long]] = + async.flatMap(_.jsonArrindex(key, path, value, range).futureLift.map(_.asScala.toList.map(Long.box(_)))) + + override def arrInsert(key: K, path: JsonPath, index: Int, value: JsonValue*): F[List[Long]] = + async.flatMap(_.jsonArrinsert(key, path, index, value: _*).futureLift.map(_.asScala.toList.map(Long.box(_)))) + + override def arrLen(key: K, path: JsonPath): F[List[Long]] = + async.flatMap(_.jsonArrlen(key, path).futureLift.map(_.asScala.toList.map(Long.box(_)))) + + override def arrPop(key: K, path: JsonPath, index: Int): F[List[JsonValue]] = + async.flatMap(_.jsonArrpop(key, path, index).futureLift.map(_.asScala.toList)) + + override def arrTrim(key: K, path: JsonPath, range: JsonRangeArgs): F[List[Long]] = + async.flatMap(_.jsonArrtrim(key, path, range).futureLift.map(_.asScala.toList.map(Long.box(_)))) + + override def toggle(key: K, path: JsonPath): F[List[Long]] = + async.flatMap(_.jsonToggle(key, path).futureLift.map(_.asScala.toList.map(Long.box(_)))) + + override def numIncrBy(key: K, path: JsonPath, number: Number): F[List[Number]] = + async.flatMap(_.jsonNumincrby(key, path, number).futureLift.map(_.asScala.toList)) + + override def mset(key: K, values: (JsonPath, JsonValue)*): F[Boolean] = { + val jValues: util.List[JsonMsetArgs[K, V]] = + values + .map { case (path, value) => new JsonMsetArgs(key, path, value) } + .asJava + .asInstanceOf[util.List[JsonMsetArgs[K, V]]] + async.flatMap(_.jsonMSet(jValues).futureLift.map(_.isSuccess)) + } + + override def set(key: K, path: JsonPath, value: JsonValue): F[Boolean] = + async.flatMap(_.jsonSet(key, path, value).futureLift).map(Option(_).exists(_.isSuccess)) + + override def setnx(key: K, path: JsonPath, value: JsonValue): F[Boolean] = + async.flatMap(_.jsonSet(key, path, value, new JsonSetArgs().nx()).futureLift.map(_.isSuccess)) + + override def setxx(key: K, path: JsonPath, value: JsonValue): F[Boolean] = + async.flatMap(_.jsonSet(key, path, value, new JsonSetArgs().xx()).futureLift.map(_.isSuccess)) + + override def jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): F[String] = + async.flatMap(_.jsonMerge(key, jsonPath, value).futureLift) + + override def strAppend(key: K, path: JsonPath, value: JsonValue): F[List[Long]] = + async.flatMap(_.jsonStrappend(key, path, value).futureLift.map(_.asScala.toList.map(x => Long.box(x)))) + + override def strLen(key: K, path: JsonPath): F[Long] = + async.flatMap(_.jsonStrlen(key, path).futureLift.map(x => Long.unbox(x))) + /******************************* Hashes API **********************************/ override def hDel(key: K, field: K, fields: K*): F[Long] = async.flatMap(_.hdel(key, (field +: fields): _*).futureLift.map(x => Long.box(x))) @@ -1282,13 +1377,13 @@ private[redis4cats] class BaseRedis[F[_]: FutureLift: MonadThrow: Log, K, V]( conn.async.flatMap(_.select(index).futureLift.void) override def auth(password: CharSequence): F[Boolean] = - async.flatMap(_.auth(password).futureLift.map(_ == "OK")) + async.flatMap(_.auth(password).futureLift.map(_.isSuccess)) override def auth(username: String, password: CharSequence): F[Boolean] = - async.flatMap(_.auth(username, password).futureLift.map(_ == "OK")) + async.flatMap(_.auth(username, password).futureLift.map(_.isSuccess)) override def setClientName(name: K): F[Boolean] = - async.flatMap(_.clientSetname(name).futureLift.map(_ == "OK")) + async.flatMap(_.clientSetname(name).futureLift.map(_.isSuccess)) override def getClientName(): F[Option[K]] = async.flatMap(_.clientGetname().futureLift).map(Option.apply) @@ -1297,10 +1392,10 @@ private[redis4cats] class BaseRedis[F[_]: FutureLift: MonadThrow: Log, K, V]( async.flatMap(_.clientId().futureLift.map(Long.unbox)) override def setLibName(name: String): F[Boolean] = - async.flatMap(_.clientSetinfo("LIB-NAME", name).futureLift.map(_ == "OK")) + async.flatMap(_.clientSetinfo("LIB-NAME", name).futureLift.map(_.isSuccess)) override def setLibVersion(version: String): F[Boolean] = - async.flatMap(_.clientSetinfo("LIB-VER", version).futureLift.map(_ == "OK")) + async.flatMap(_.clientSetinfo("LIB-VER", version).futureLift.map(_.isSuccess)) override def getClientInfo: F[Map[String, String]] = async.flatMap( @@ -1694,6 +1789,10 @@ private[redis4cats] trait RedisConversionOps { } } + private[redis4cats] implicit class ResponseOps(str: String) { + def isSuccess: Boolean = str == "OK" + } + } private[redis4cats] class Redis[F[_]: FutureLift: MonadThrow: Log, K, V](