packages feed

redis-schema-0.1.0: src/Database/Redis/Schema.hs

{-# LANGUAGE Strict #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-} -- for (RefInstance ref) in constraints in instance head
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for Hedis.RedisResult (a,b,c)

-- | The schema-based Redis module.
--   This module is intended to be imported qualified.
--   That's why we don't have 'RedisRef' but rather 'Redis.Ref'.
module Database.Redis.Schema
  ( Pool(..), RedisM(..)
    -- Pool and RedisM export their internals so other libraries can provide combinators
    -- like runNonBlocking or others. These internals are not meant to be used ordinarily.
  , Redis, Instance, DefaultInstance
  , Tx, atomically, runTx
  , RedisException(..)
  , Ref(..), Value(..)
  , SimpleRef, SimpleValue, SimpleValueIdentifier(..), Serializable(..), Serializables(..)
  , TTL(..)
  , run
  , connect
  , incrementBy, incrementByFloat
  , txIncrementBy
  , get, set, getSet
  , txGet, txSet, txExpect
  , setWithTTL, setIfNotExists, setIfNotExists_
  , txSetWithTTL, txSetIfNotExists, txSetIfNotExists_
  , delete_, txDelete_
  , Database.Redis.Schema.take, txTake
  , setTTL, setTTLIfExists, setTTLIfExists_
  , txSetTTL, txSetTTLIfExists, txSetTTLIfExists_
  , readBS, showBS
  , showBinary, readBinary, colonSep
  , Tuple(..)
  , day, hour, minute, second
  , throw, throwMsg
  , sInsert, sDelete, sContains, sSize
  , Priority(..), zInsert, zSize, zCount, zDelete, zPopMin, bzPopMin, zRangeByScoreLimit
  , txSInsert, txSDelete, txSContains, txSSize
  , MapItem(..)
  , RecordField(..), RecordItem(..), Record
  , lLength, lAppend, txLAppend, lPushLeft, lPopRight, lPopRightBlocking, lRem
  , watch, unwatch
  , unliftIO
  , deleteIfEqual, setIfNotExistsTTL
  , PubSub, pubSubListen, pubSubCountSubs
  ) where

import GHC.Word         ( Word32  )
import Data.Functor     ( void, (<&>) )
import Data.Function    ( (&) )
import Data.Time        ( UTCTime, LocalTime, Day )
import Text.Read        ( readMaybe )
import Data.ByteString  ( ByteString )
import Data.Binary      ( Binary, encode, decodeOrFail )
import Data.Text        ( Text )
import Data.Text.Encoding ( encodeUtf8, decodeUtf8 )
import Data.Kind        ( Type )
import Data.Map         ( Map )
import Data.Set         ( Set )
import Data.Int         ( Int64 )
import Data.UUID        ( UUID )
import qualified Data.UUID as UUID

import Control.Applicative
import qualified Control.Arrow as Arrow
import Control.Monad        ( (<=<) )
import Control.Exception    ( throwIO, Exception )
import Control.Monad.Reader ( runReaderT, ask )
import Control.Monad.IO.Class ( liftIO, MonadIO )

import qualified Numeric.Limits
import qualified Database.Redis as Hedis
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified System.IO.Error as IOE

-- | Each instance has a distinct connection pool type.
-- (Hedis names it Connection but it's a pool.)
newtype Pool inst = Pool{_unPool :: Hedis.Connection}

-- | Instance-indexed monad for Redis computations.
newtype RedisM inst a = Redis{unRedis :: Hedis.Redis a}
 deriving newtype (Functor, Applicative, Monad, MonadIO, Hedis.MonadRedis)

-- | The kind of Redis instances. Ideally, this would be a user-defined DataKind,
--   but since Haskell does not have implicit arguments,
--   that would require that we index everything with it explicitly,
--   which would create a lot of syntactic noise.
--
--   (Ab)using the Type kind for instances is a compromise.
type Instance = Type

-- | We also define a default instance.
--   This is convenient for code bases using only one Redis instance,
--   since 'RefInstance' defaults to this. (See the 'Ref' typeclass below.)
data DefaultInstance

-- | The Redis monad related to the default instance.
type Redis = RedisM DefaultInstance

instance Hedis.RedisCtx (RedisM inst) (Either Hedis.Reply) where
  returnDecode = Redis . Hedis.returnDecode

data RedisException
  = BadConnectionString String String
  | CouldNotPing String
  | UnexpectedResult String String
  | UserException String
  | TransactionAborted
  | TransactionError String
  | CouldNotDecodeValue (Maybe ByteString)
  | LockAcquireTimeout
  | UnexpectedStatus String Hedis.Status
  | EmptyAlternative  -- for 'instance Alternative Tx'
  deriving (Show, Exception)

-- | Time-To-Live for Redis values. The Num instance works in (integral) seconds.
newtype TTL = TTLSec { ttlToSeconds :: Integer }
  deriving newtype (Eq, Ord, Num)

run :: MonadIO m => Pool inst -> RedisM inst a -> m a
run (Pool pool) = liftIO . Hedis.runRedis pool . unRedis

throw :: RedisException -> RedisM inst a
throw = liftIO . throwIO

throwMsg :: String -> RedisM inst a
throwMsg = throw . UserException

-- | Expect Right, otherwise throw UnexpectedResult.
expectRight :: Show e => String -> Either e a -> RedisM inst a
expectRight _msg (Right x) = pure x
expectRight  msg (Left e) = throw $ UnexpectedResult ("Redis.expectRight: " ++ msg) (show $ left e)
  where
    -- hard to give this type to Left inline
    left :: e -> Either e ()
    left = Left

-- | Expect transaction success, otherwise throw.
expectTxSuccess :: Hedis.TxResult a -> RedisM inst a
expectTxSuccess (Hedis.TxSuccess x) = pure x
expectTxSuccess  Hedis.TxAborted    = throw TransactionAborted
expectTxSuccess (Hedis.TxError err) = throw $ TransactionError err

-- | Expect exact value, otherwise throw UnexpectedResult.
expect :: (Eq a, Show a) => String -> a -> a -> RedisM inst ()
expect msg expected actual
  | expected == actual = pure ()
  | otherwise = throw $ UnexpectedResult ("Redis.expect: " ++ msg) (show actual)

-- Useful in combination with the expect* functions.
ignore :: a -> RedisM inst ()
ignore _ = pure ()

-- | Open a connection pool to redis
connect :: String -> Int -> IO (Pool inst)
connect connectionString poolSize =
  case Hedis.parseConnectInfo connectionString of
    Left err -> throwIO $ BadConnectionString connectionString err
    Right connInfo -> do
      pool <- Hedis.connect connInfo
        { Hedis.connectMaxConnections = poolSize
        }
      customizeIOError connectionString (Hedis.runRedis pool Hedis.ping) >>= \case
        Right Hedis.Pong -> return (Pool pool)
        resp -> throwIO $ CouldNotPing (show resp)
  where
    -- Runs an IO action and prepends a custom error message to any occuring IOError
    customizeIOError :: String -> IO a -> IO a
    customizeIOError errorMessage action = IOE.modifyIOError customError action
      where
      customError :: IOError -> IOError
      customError err = IOE.ioeSetErrorString err (errorMessage <> "; " <> IOE.ioeGetErrorString err)

-- | Redis transactions.
--
-- In comparison with Hedis transactions:
--
-- * 'Tx' is newtyped as a separate functor for clearer types and better error messages.
--
-- * 'Tx' is not a monad, just an 'Applicative' functor.
--   Applicative exactly corresponds to the nature of Redis transactions,
--   and does not need 'Queued' hacks.
--
-- * 'Tx' supports throwing, and catching via 'Alternative'.
--   Beware that 'Tx' is 'Applicative' so all side effects will be carried out,
--   whether any actions throw or not. Throwing and catching is done at the level
--   where the _results_ of the individual applicative actions are composed.
--
-- You can still have do-notation with the @ApplicativeDo@ extension.
newtype Tx inst a = Tx
  { unTx :: Hedis.RedisTx (Hedis.Queued (Either RedisException a))
  }

instance Functor (Tx inst) where
  fmap f (Tx tx) = Tx $ fmap (fmap (fmap f)) tx

instance Applicative (Tx inst) where
  pure x = Tx $ pure (pure (pure x))
  Tx txF <*> Tx txX = Tx $ do
    queuedF <- txF
    queuedX <- txX
    pure $ do
      eitherF <- queuedF
      eitherX <- queuedX
      pure (eitherF <*> eitherX)

instance Alternative (Tx inst) where
  empty = txThrow EmptyAlternative
  Tx txX <|> Tx txY = Tx $ do
    queuedX <- txX
    queuedY <- txY
    pure $ do
      eitherX <- queuedX
      eitherY <- queuedY
      pure $ case eitherX of
        Right x -> Right x
        Left _err -> case eitherY of
          Right y -> Right y
          Left err -> Left err

-- | Run a Redis transaction and return its result.
--
-- Most code will probably want to use 'atomically' instead,
-- which automatically propagates errors.
runTx :: Tx inst a -> RedisM inst (Hedis.TxResult (Either RedisException a))
runTx = Redis . Hedis.multiExec . unTx

-- | Throw in a transaction.
txThrow :: RedisException -> Tx inst a
txThrow e = Tx $ pure (pure (Left e))

-- | Embed a raw Hedis action in a 'Tx' transaction.
txWrap :: Hedis.RedisTx (Hedis.Queued a) -> Tx inst a
txWrap action = Tx (fmap Right <$> action)

-- | Run a 'Tx' transaction, propagating any errors.
atomically :: Tx inst a -> RedisM inst a
atomically tx = runTx tx >>= expectTxSuccess >>= \case
  Right x -> pure x
  Left  e -> throw e

-- | Apply a possibly failing computation to the result of a transaction.
--
-- Useful for implementation of various checks.
txCheckMap :: (a -> Either RedisException b) -> Tx inst a -> Tx inst b
txCheckMap f (Tx tx) = Tx (fmap (fmap g) tx)
  where
    g (Left e) = Left e  -- we already had an error here
    g (Right x) = f x    -- possibly fail

-- | Expect an exact value.
txExpect :: (Eq a, Show a) => String -> a -> Tx inst a -> Tx inst ()
txExpect msg expected = void . txCheckMap f
  where
    f x | x == expected = Right x
        | otherwise = Left $ UnexpectedResult msg (show x)

-- | Reference to some abstract Redis value.
--
-- 'ByteString's are inappropriate for this purpose:
--
-- * 'Ref's are typed.
--
-- * bytestring concatenation and other faffing is ugly and error-prone.
--
-- * some values may be stored across several Redis keys,
--   (such as Tiers.Redis.Profile),
--   in which case bytestrings are not even sufficient.
--
-- All methods have defaults for easy implementation of 'SimpleValue's for new types.
-- For simple values, it's sufficient to implement (or newtype-derive) 'SimpleValue',
-- and declare an empty @instance Value <TheType>@.
class Value (RefInstance ref) (ValueType ref) => Ref ref where
  -- | Type of the value that this ref points to.
  type ValueType ref :: Type

  -- | RedisM instance this ref points into, with a default.
  type RefInstance ref :: Instance
  type RefInstance ref = DefaultInstance

  -- | How to convert the ref to an identifier that its value accepts.
  toIdentifier :: ref -> Identifier (ValueType ref)

-- | Type that can be read/written from Redis.
--
-- This can be a simple value, such as string or integer, or a composite value,
-- such as a complex record stored across multiple keys, hashes, sets and lists.
--
-- We parameterise the typeclass with the Redis instance.
-- Most Value instances will want to keep 'inst' open
-- but some may need to restrict it to a particular Redis instance;
-- especially those that access Refs under the hood, since Refs are instance-specific.
class Value inst val where
  -- | How the value is identified in Redis.
  --
  -- Types like hashes, sets or list are always top-level keys in Redis,
  -- so these are identified by bytestrings. Simple values can be top-level
  -- or hash fields, so they are identified by SimpleValueIdentifier.
  -- Complex values may be identified by something else; for example
  -- 'Tiers.Redis.Profile' is identified by a 'Tiers.Token',
  -- because it's a complex value spread across multiple Redis keys.
  type Identifier val :: Type
  type Identifier val = SimpleValueIdentifier  -- default


  -- | Read a value from Redis in a transaction.
  txValGet :: Identifier val -> Tx inst (Maybe val)

  default txValGet :: SimpleValue inst val => Identifier val -> Tx inst (Maybe val)
  txValGet (SviTopLevel keyBS) = fmap (fromBS =<<) . txWrap $ Hedis.get keyBS
  txValGet (SviHash keyBS hkeyBS) = fmap (fromBS =<<) . txWrap $ Hedis.hget keyBS hkeyBS

  -- | Write a value to Redis in a transaction.
  txValSet :: Identifier val -> val -> Tx inst ()

  default txValSet :: SimpleValue inst val => Identifier val -> val -> Tx inst ()
  txValSet (SviTopLevel keyBS) val =
    txExpect "txValSet/plain" Hedis.Ok
      $ txWrap (Hedis.set keyBS $ toBS val)
  txValSet (SviHash keyBS hkeyBS) val =
    void
      $ txWrap (Hedis.hset keyBS hkeyBS $ toBS val)

  -- | Delete a value from Redis in a transaction.
  txValDelete :: Identifier val -> Tx inst ()

  default txValDelete :: SimpleValue inst val => Identifier val -> Tx inst ()
  txValDelete (SviTopLevel keyBS) = void . txWrap $ Hedis.del [keyBS]
  txValDelete (SviHash keyBS hkeyBS) = void . txWrap $ Hedis.hdel keyBS [hkeyBS]

  -- | Set time-to-live for a value in a transaction. Return 'True' if the value exists.
  txValSetTTLIfExists :: Identifier val -> TTL -> Tx inst Bool

  default txValSetTTLIfExists :: SimpleValue inst val => Identifier val -> TTL -> Tx inst Bool
  txValSetTTLIfExists (SviTopLevel keyBS) (TTLSec ttlSec) =
    txWrap $ Hedis.expire keyBS ttlSec
  txValSetTTLIfExists (SviHash keyBS _hkeyBS) (TTLSec ttlSec) =
    txWrap $ Hedis.expire keyBS ttlSec


  -- | Read a value.
  valGet :: Identifier val -> RedisM inst (Maybe val)

  default valGet :: SimpleValue inst val => Identifier val -> RedisM inst (Maybe val)
  valGet (SviTopLevel keyBS) =
    fmap (fromBS =<<) . expectRight "valGet/plain" =<< Hedis.get keyBS
  valGet (SviHash keyBS hkeyBS) =
    fmap (fromBS =<<) . expectRight "valGet/hash" =<< Hedis.hget keyBS hkeyBS

  -- | Write a value.
  valSet :: Identifier val -> val -> RedisM inst ()

  default valSet :: SimpleValue inst val => Identifier val -> val -> RedisM inst ()
  valSet (SviTopLevel keyBS) val =
    expect "valSet/plain" (Right Hedis.Ok) =<< Hedis.set keyBS (toBS val)
  valSet (SviHash keyBS hkeyBS) val =
    ignore {- @Integer -} =<< expectRight "valSet/hash" =<< Hedis.hset keyBS hkeyBS (toBS val)
      --   ^- this is Bool in some versions of Hedis and Integer in others

  -- | Delete a value.
  valDelete :: Identifier val -> RedisM inst ()

  default valDelete :: SimpleValue inst val => Identifier val -> RedisM inst ()
  valDelete (SviTopLevel keyBS) =
    ignore @Integer =<< expectRight "valDelete/plain" =<< Hedis.del [keyBS]
  valDelete (SviHash keyBS hkeyBS) =
    ignore @Integer =<< expectRight "valDelete/hash" =<< Hedis.hdel keyBS [hkeyBS]

  -- | Set time-to-live for a value. Return 'True' if the value exists.
  valSetTTLIfExists :: Identifier val -> TTL -> RedisM inst Bool

  default valSetTTLIfExists :: SimpleValue inst val => Identifier val -> TTL -> RedisM inst Bool
  valSetTTLIfExists (SviTopLevel keyBS) (TTLSec ttlSec) =
    expectRight "valSetTTLIfExists/plain" =<< Hedis.expire keyBS ttlSec
  valSetTTLIfExists (SviHash keyBS _hkeyBS) (TTLSec ttlSec) =
    expectRight "valSetTTLIfExists/hash" =<< Hedis.expire keyBS ttlSec

data SimpleValueIdentifier
  = SviTopLevel ByteString         -- ^ Stored in a top-level key.
  | SviHash ByteString ByteString  -- ^ Stored in a hash field.

-- | Simple values, like strings, integers or enums,
-- that be represented as a single bytestring.
--
-- Of course, any value can be represented as a single bytestring,
-- but structures like lists, hashes and sets have special support in Redis.
-- This allows insertions, updates, etc. in Redis directly,
-- but they cannot be read or written as bytestrings, and thus are not 'SimpleValue's.
class (Value inst val, Identifier val ~ SimpleValueIdentifier, Serializable val) => SimpleValue inst val

class Serializable val where
  fromBS :: ByteString -> Maybe val
  toBS :: val -> ByteString

-- | 'Ref' pointing to a 'SimpleValue'.
type SimpleRef ref = (Ref ref, SimpleValue (RefInstance ref) (ValueType ref))

get :: Ref ref => ref -> RedisM (RefInstance ref) (Maybe (ValueType ref))
get = valGet . toIdentifier

txGet :: Ref ref => ref -> Tx (RefInstance ref) (Maybe (ValueType ref))
txGet = txValGet . toIdentifier

set :: Ref ref => ref -> ValueType ref -> RedisM (RefInstance ref) ()
set = valSet . toIdentifier

txSet :: Ref ref => ref -> ValueType ref -> Tx (RefInstance ref) ()
txSet = txValSet . toIdentifier

delete_ :: forall ref. Ref ref => ref -> RedisM (RefInstance ref) ()
delete_ = valDelete @_ @(ValueType ref) . toIdentifier

txDelete_ :: forall ref. Ref ref => ref -> Tx (RefInstance ref) ()
txDelete_ = txValDelete @_ @(ValueType ref) . toIdentifier

-- | Atomically read and delete.
take :: Ref ref => ref -> RedisM (RefInstance ref) (Maybe (ValueType ref))
take ref = atomically (txTake ref)

-- | Atomically read and delete in a transaction.
txTake :: Ref ref => ref -> Tx (RefInstance ref) (Maybe (ValueType ref))
txTake ref = txGet ref <* txDelete_ ref

-- | Atomically set a value and return its old value.
getSet :: forall ref. SimpleRef ref => ref -> ValueType ref -> RedisM (RefInstance ref) (Maybe (ValueType ref))
getSet ref val = case toIdentifier ref of
  SviTopLevel keyBS ->
    fmap (fromBS =<<) . expectRight "getSet/plain"
      =<< Hedis.getset keyBS (toBS val)

  -- no native Redis call for this
  SviHash _ _ -> atomically (txGet ref <* txSet ref val)

-- | Bump the TTL without changing the content.
setTTLIfExists :: forall ref. Ref ref => ref -> TTL -> RedisM (RefInstance ref) Bool
setTTLIfExists = valSetTTLIfExists @_ @(ValueType ref) . toIdentifier

setTTLIfExists_ :: Ref ref => ref -> TTL -> RedisM (RefInstance ref) ()
setTTLIfExists_ ref = void . setTTLIfExists ref

setTTL :: Ref ref => ref -> TTL -> RedisM (RefInstance ref) ()
setTTL ref ttl = setTTLIfExists ref ttl >>= expect "setTTL: ref should exist" True

txSetTTLIfExists :: forall ref. Ref ref => ref -> TTL -> Tx (RefInstance ref) Bool
txSetTTLIfExists = txValSetTTLIfExists @_ @(ValueType ref) . toIdentifier

txSetTTLIfExists_ :: forall ref. Ref ref => ref -> TTL -> Tx (RefInstance ref) ()
txSetTTLIfExists_ ref ttl = void $ txSetTTLIfExists ref ttl

txSetTTL :: Ref ref => ref -> TTL -> Tx (RefInstance ref) ()
txSetTTL ref ttl =
  txSetTTLIfExists ref ttl
    & txExpect "txSetTTL: ref should exist" True

txSetWithTTL :: SimpleRef ref => ref -> TTL -> ValueType ref -> Tx (RefInstance ref) ()
txSetWithTTL ref ttl val = txSet ref val *> txSetTTL ref ttl

-- | Set value and TTL atomically.
setWithTTL :: forall ref. SimpleRef ref => ref -> TTL -> ValueType ref  -> RedisM (RefInstance ref) ()
setWithTTL ref ttl@(TTLSec ttlSec) val = case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.setex keyBS ttlSec (toBS val)
    >>= expectRight "setWithTTL/SETEX"
    >>= expect "setWithTTL/SETEX should return OK" Hedis.Ok
  SviHash _ _ -> atomically (txSet ref val <* txSetTTL ref ttl)

-- | Increment the value under the given ref.
incrementBy :: (SimpleRef ref, Num (ValueType ref)) => ref -> Integer -> RedisM (RefInstance ref) (ValueType ref)
incrementBy ref val = fmap fromInteger . expectRight "incrementBy" =<< case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.incrby keyBS val
  SviHash keyBS hkeyBS -> Hedis.hincrby keyBS hkeyBS val

txIncrementBy :: (SimpleRef ref, Num (ValueType ref)) => ref -> Integer -> Tx (RefInstance ref) (ValueType ref)
txIncrementBy ref val = fmap fromInteger . txWrap $ case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.incrby keyBS val
  SviHash keyBS hkeyBS -> Hedis.hincrby keyBS hkeyBS val

-- | Increment the value under the given ref.
incrementByFloat :: (SimpleRef ref, Floating (ValueType ref)) => ref -> Double -> RedisM (RefInstance ref) (ValueType ref)
incrementByFloat ref val = fmap realToFrac . expectRight "incrementByFloat" =<< case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.incrbyfloat keyBS val
  SviHash keyBS hkeyBS -> Hedis.hincrbyfloat keyBS hkeyBS val

setIfNotExists :: forall ref. SimpleRef ref => ref -> ValueType ref -> RedisM (RefInstance ref) Bool
setIfNotExists ref val = expectRight "setIfNotExists" =<< case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.setnx keyBS (toBS val)
  SviHash keyBS hkeyBS -> Hedis.hsetnx keyBS hkeyBS (toBS val)

setIfNotExists_ :: SimpleRef ref => ref -> ValueType ref -> RedisM (RefInstance ref) ()
setIfNotExists_ ref val = void $ setIfNotExists ref val

txSetIfNotExists :: forall ref. SimpleRef ref => ref -> ValueType ref -> Tx (RefInstance ref) Bool
txSetIfNotExists ref val = txWrap $ case toIdentifier ref of
  SviTopLevel keyBS -> Hedis.setnx keyBS (toBS val)
  SviHash keyBS hkeyBS -> Hedis.hsetnx keyBS hkeyBS (toBS val)

txSetIfNotExists_ :: SimpleRef ref => ref -> ValueType ref -> Tx (RefInstance ref) ()
txSetIfNotExists_ ref val = void $ txSetIfNotExists ref val

setIfNotExistsTTL :: forall ref. SimpleRef ref => ref -> ValueType ref -> TTL -> RedisM (RefInstance ref) Bool
setIfNotExistsTTL ref val (TTLSec ttlSec) =
  (== Right Hedis.Ok) <$> case toIdentifier ref of
    SviHash _keyBS _hkeyBS -> error "setIfNotExistsTTL: hash keys not supported"
    SviTopLevel keyBS -> Hedis.setOpts keyBS (toBS val) Hedis.SetOpts
      { Hedis.setSeconds      = Just ttlSec
      , Hedis.setMilliseconds = Nothing
      , Hedis.setCondition    = Just Hedis.Nx
      }

deleteIfEqual :: forall ref. SimpleRef ref => ref -> ValueType ref -> RedisM (RefInstance ref) Bool
deleteIfEqual ref val =
  fmap (/= (0 :: Integer)) . expectRight "deleteIfEqual" =<< case toIdentifier ref of
    SviHash _keyBS _hkeyBS -> error "deleteIfEqual: hash keys not supported"
    SviTopLevel keyBS -> Hedis.eval luaSource [keyBS] [toBS val]
  where
    luaSource :: ByteString
    luaSource = BS.unlines
      [ "if redis.call(\"get\",KEYS[1]) == ARGV[1] then"
      , "  return redis.call(\"del\",KEYS[1])"
      , "else"
      , "  return 0"
      , "end"
      ]

-- | Make any subsequent transaction fail if the watched ref is modified
-- between the call to 'watch' and the transaction.
watch :: SimpleRef ref => ref -> RedisM (RefInstance ref) ()
watch ref = case toIdentifier ref of
  SviTopLevel keyBS ->
    Redis (Hedis.watch [keyBS]) >>= expect "watch/plain: OK expected" (Right Hedis.Ok)
  SviHash keyBS _hkeyBS ->
    Redis (Hedis.watch [keyBS]) >>= expect "watch/hash: OK expected" (Right Hedis.Ok)

-- | Unwatch all watched keys.
-- I can't find it anywhere in the documentation
-- but I hope that this unwatches only the keys for the current connection,
-- and does not affect other connections. Nothing else would make much sense.
unwatch :: RedisM inst ()
unwatch = Redis Hedis.unwatch >>= expect "unwatch: OK expected" (Right Hedis.Ok)

-- | Decode a list of ByteStrings.
-- On failure, return the first ByteString that could not be decoded.
fromBSMany :: Serializable val => [ByteString] -> Either ByteString [val]
fromBSMany = traverse $ \valBS -> case fromBS valBS of
  Just val -> Right val    -- decoded correctly
  Nothing  -> Left  valBS  -- decoding failure, return the malformed bytestring

txFromBSMany :: Serializable val => Tx inst [ByteString] -> Tx inst [val]
txFromBSMany = txCheckMap (f . fromBSMany)
  where
    f (Left badBS) = Left $ CouldNotDecodeValue (Just badBS)
    f (Right vals) = Right vals

instance Value inst ()
instance Serializable () where
  fromBS = const $ Just ()
  toBS = const ""
instance SimpleValue inst ()

{- conflicts with the [a] instance
instance Value inst String
instance Serializable String where
  fromBS = fmap Text.unpack . fromBS
  toBS = toBS . Text.pack
-}

instance Value inst Text
instance Serializable Text where
  fromBS = Just . decodeUtf8
  toBS = encodeUtf8
instance SimpleValue inst Text

instance Value inst Int
instance Serializable Int where
  fromBS = readBS
  toBS   = showBS
instance SimpleValue inst Int

instance Value inst Word32
instance Serializable Word32 where
  fromBS = readBS
  toBS   = showBS
instance SimpleValue inst Word32

instance Value inst Int64
instance Serializable Int64 where
  fromBS = readBS
  toBS   = showBS
instance SimpleValue inst Int64

instance Value inst Integer
instance Serializable Integer where
  fromBS = readBS
  toBS   = showBS
instance SimpleValue inst Integer

instance Value inst Double
instance Serializable Double where
  fromBS = readBS
  toBS   = showBS
instance SimpleValue inst Double

instance Value inst Bool
instance Serializable Bool where
  fromBS "0" = Just False
  fromBS "1" = Just True
  fromBS _ = Nothing

  toBS True  = "1"
  toBS False = "0"
instance SimpleValue inst Bool

instance Value inst UTCTime
instance Serializable UTCTime where
  fromBS = readBS
  toBS = showBS
instance SimpleValue inst UTCTime

instance Value inst Day
instance Serializable Day where
  fromBS = readBS
  toBS = showBS
instance SimpleValue inst Day

instance Value inst LocalTime
instance Serializable LocalTime where
  fromBS = readBS
  toBS = showBS
instance SimpleValue inst LocalTime

instance Value inst ByteString
instance Serializable ByteString where
  toBS   = id
  fromBS = Just
instance SimpleValue inst ByteString

instance Value inst BSL.ByteString
instance Serializable BSL.ByteString where
  toBS   = BSL.toStrict
  fromBS = Just . BSL.fromStrict
instance SimpleValue inst BSL.ByteString

instance Serializable UUID where
  toBS = toBS . UUID.toText
  fromBS = UUID.fromText <=< fromBS

instance Serializable a => Serializable (Maybe a) where
  fromBS b = case BS.uncons b of
    Just ('N', "") -> Just Nothing -- parsing succeeded, found Nothing
    Just ('J', r)  -> Just <$> fromBS r
    _              -> Nothing -- Parsing failed
  toBS Nothing  = "N"
  toBS (Just a) = "J" <> toBS a

instance (Serializable a, Serializable b) => Serializable (Either a b) where
  fromBS b = case BS.uncons b of
    Just ('L', xBS) -> Left <$> fromBS xBS
    Just ('R', yBS) -> Right <$> fromBS yBS
    _ -> Nothing
  toBS (Left x) = BS.cons 'L' (toBS x)
  toBS (Right y) = BS.cons 'R' (toBS y)

instance (SimpleValue inst a, SimpleValue inst b) => Value inst (a, b)
instance (Serializable a, Serializable b) => Serializable (a, b) where
  toBS (x, y) = toBS @(Tuple '[a,b]) (x :*: y :*: Nil)
  fromBS bs =
    fromBS @(Tuple '[a,b]) bs <&>
      \(x :*: y :*: Nil) -> (x,y)
instance (SimpleValue inst a, SimpleValue inst b) => SimpleValue inst (a,b)

instance (SimpleValue inst a, SimpleValue inst b, SimpleValue inst c) => Value inst (a, b, c)
instance (Serializable a, Serializable b, Serializable c) => Serializable (a, b, c) where
  toBS (x, y, z) = toBS (x :*: y :*: z :*: Nil)
  fromBS bs =
    fromBS @(Tuple '[a,b,c]) bs <&>
      \(x :*: y :*: z :*: Nil) -> (x,y,z)
instance (SimpleValue inst a, SimpleValue inst b, SimpleValue inst c) => SimpleValue inst (a, b, c)

readBS :: Read val => ByteString -> Maybe val
readBS = readMaybe . BS.unpack

showBS :: Show val => val -> ByteString
showBS = BS.pack . show

showBinary :: Binary val => val -> ByteString
showBinary = BSL.toStrict . encode

readBinary :: Binary val => ByteString -> Maybe val
readBinary bytes = case decodeOrFail $ BSL.fromStrict bytes of
  Left _ -> Nothing
  Right (_, _, val) -> Just val

colonSep :: [BS.ByteString] -> BS.ByteString
colonSep = BS.intercalate ":"

infixr 3 :*:
data Tuple :: [Type] -> Type where
  Nil :: Tuple '[]
  (:*:) :: a -> Tuple as -> Tuple (a ': as)

instance Eq (Tuple '[]) where
  _ == _ = True

instance Ord (Tuple '[]) where
  compare _ _ = EQ

instance (Eq a, Eq (Tuple as)) => Eq (Tuple (a ': as)) where
  (x :*: xs) == (y :*: ys) = x == y && xs == ys

instance (Ord a, Ord (Tuple as)) => Ord (Tuple (a ': as)) where
  compare (x :*: xs) (y :*: ys) = compare x y <> compare xs ys

class Serializables (as :: [Type]) where
  encodeSerializables :: Tuple as -> [BS.ByteString]
  decodeSerializables :: [BS.ByteString] -> Maybe (Tuple as)

instance Serializables '[] where
  encodeSerializables Nil = []

  decodeSerializables [] = Just Nil
  decodeSerializables _  = Nothing

instance (Serializable a, Serializables as) => Serializables (a ': as) where
  encodeSerializables (x :*: xs) = toBS x : encodeSerializables xs

  decodeSerializables [] = Nothing
  decodeSerializables (bs : bss) = (:*:) <$> fromBS bs <*> decodeSerializables bss

instance Serializables as => Value inst (Tuple as)
instance Serializables as => Serializable (Tuple as) where
  toBS = encodeBSs . encodeSerializables
    where
      -- Encode a list of bytestrings into a single bytestring
      -- that's unambiguous (for machines) but human-readable (for humans).
      --
      -- This is useful for tuples and records
      -- that you need to put in a Redis list or a Redis set
      -- so they need to be Serializables.
      --
      -- The format:
      --   <length1>,<length2>,...,<lengthN>:<string1>:<string2>:...:<stringN>
      --
      -- Lengths are base10 numbers, strings are literal binary strings.
      encodeBSs :: [BS.ByteString] -> BS.ByteString
      encodeBSs bss = BS.intercalate ":" (lengths : bss)
        where
          lengths = BS.intercalate "," [BS.pack (show (BS.length bs)) | bs <- bss]

  fromBS = decodeSerializables <=< decodeBSs
    where
      decodeBSs :: BS.ByteString -> Maybe [BS.ByteString]
      decodeBSs bsWhole = do
          lengths <- traverse fromBS $ BS.split ',' bsLengths
          splitLengths lengths bsData
        where
          -- bsData starts with a colon
          (bsLengths, bsData) = BS.span (/= ':') bsWhole

          splitLengths [] "" = Just []
          splitLengths [] _trailingGarbage = Nothing
          splitLengths (l:ls) bs = case BS.uncons bs of
            Just (':', bsNoColon) ->
              let (item, rest) = BS.splitAt l bsNoColon
                in (item :) <$> splitLengths ls rest

            _ -> Nothing
instance Serializables as => SimpleValue inst (Tuple as)

day :: TTL
day = 24 * hour

hour :: TTL
hour = 60 * minute

minute :: TTL
minute = 60 * second

second :: TTL
second = TTLSec 1

-- | Redis lists.
instance Serializable a => Value inst [a] where
  type Identifier [a] = ByteString

  txValGet keyBS =
    txWrap (Hedis.lrange keyBS 0 (-1))
    & txFromBSMany
    & fmap Just
  txValSet keyBS vs = void $ txWrap (Hedis.del [keyBS] *> Hedis.rpush keyBS (map toBS vs))
  txValDelete keyBS = void $ txWrap (Hedis.del [keyBS])
  txValSetTTLIfExists keyBS (TTLSec ttlSec) = txWrap (Hedis.expire keyBS ttlSec)

  valGet keyBS =
    Redis (Hedis.lrange keyBS 0 (-1))
      >>= expectRight "valGet/[a]"
      >>= (fromBSMany <&> \case
        Left badBS -> throw $ CouldNotDecodeValue (Just badBS)
        Right vs -> pure (Just vs))

  valSet keyBS vs =
    Redis (Hedis.multiExec (Hedis.del [keyBS] *> Hedis.rpush keyBS (map toBS vs)))
      >>= expectTxSuccess
      >>= ignore @Integer
  valDelete keyBS =
    Redis (Hedis.del [keyBS])
      >>= expectRight "valDelete/[a]"
      >>= ignore @Integer
  valSetTTLIfExists keyBS (TTLSec ttlSec) =
    Redis (Hedis.expire keyBS ttlSec)
      >>= expectRight "valSetTTLIfExists/[a]"

-- | Append to a Redis list.
lAppend :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> [a] -> RedisM (RefInstance ref) ()
lAppend (toIdentifier -> keyBS) vals =
  Redis (Hedis.rpush keyBS (map toBS vals))
    >>= expectRight "rpush"
    >>= ignore @Integer

-- | Append to a Redis list in a transaction.
txLAppend :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> [a] -> Tx (RefInstance ref) ()
txLAppend (toIdentifier -> keyBS) vals =
  void . txWrap $ Hedis.rpush keyBS (map toBS vals)

-- | Length of a Redis list
lLength :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> RedisM (RefInstance ref) Integer
lLength (toIdentifier -> keyBS) =
  Redis (Hedis.llen keyBS)
    >>= expectRight "llen"

-- | Prepend to a Redis list.
lPushLeft :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> [a] -> RedisM (RefInstance ref) ()
lPushLeft (toIdentifier -> keyBS) vals =
  Redis (Hedis.lpush keyBS (map toBS vals))
    >>= expectRight "lpush"
    >>= ignore @Integer

-- | Pop from the right.
lPopRight :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> RedisM (RefInstance ref) (Maybe a)
lPopRight (toIdentifier -> keyBS) =
  Redis (Hedis.rpop keyBS)
  >>= fmap (fromBS =<<) . expectRight "rpop"

-- | Pop from the right, blocking.
lPopRightBlocking :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => TTL -> ref -> RedisM (RefInstance ref) (Maybe a)
lPopRightBlocking (TTLSec timeoutSec) (toIdentifier -> keyBS) =
  Redis (Hedis.brpop [keyBS] timeoutSec)
    >>= expectRight "brpop"
    >>= \case
      Nothing -> pure Nothing -- timeout
      Just (_listName, valBS) ->
        case fromBS valBS of
          Just val -> pure $ Just val
          Nothing -> throw $ CouldNotDecodeValue (Just valBS)

-- | Delete from a Redis list
lRem :: forall ref a. (Ref ref, ValueType ref ~ [a], Serializable a) => ref -> Integer -> a -> RedisM (RefInstance ref) ()
lRem (toIdentifier -> keyBS) num val =
  Redis (Hedis.lrem keyBS num (toBS val))
    >>= expectRight "lrem"
    >>= ignore @Integer


-- | Redis sets.
instance (Serializable a, Ord a) => Value inst (Set a) where
  type Identifier (Set a) = ByteString

  txValGet keyBS =
    txWrap (Hedis.smembers keyBS)
    & txFromBSMany
    & fmap (Just . Set.fromList)

  txValSet keyBS vs =
    void $ txWrap (
      Hedis.del [keyBS]
      *> Hedis.sadd keyBS (map toBS $ Set.toList vs)
    )

  txValDelete keyBS = void $ txWrap (Hedis.del [keyBS])
  txValSetTTLIfExists keyBS (TTLSec ttlSec) = txWrap (Hedis.expire keyBS ttlSec)

  valGet keyBS =
    Hedis.smembers keyBS
      >>= expectRight "valGet/Set a"
      >>= (fromBSMany <&> \case
        Left badBS -> throw $ CouldNotDecodeValue (Just badBS)
        Right vs -> pure (Just $ Set.fromList vs))

  valSet keyBS vs =
    Redis (Hedis.multiExec (
      Hedis.del [keyBS]
      *> Hedis.sadd keyBS (map toBS $ Set.toList vs)
    ))
      >>= expectTxSuccess
      >>= ignore @Integer

  valDelete keyBS = Redis (Hedis.del [keyBS])
    >>= expectRight "valDelete/Set a"
    >>= ignore @Integer

  valSetTTLIfExists keyBS (TTLSec ttlSec) =
    Redis (Hedis.expire keyBS ttlSec)
      >>= expectRight "valSetTTLIfExists/Set a"

-- | Insert into a Redis set.
sInsert :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> [a] -> RedisM (RefInstance ref) ()
sInsert ref vals =
  Redis (Hedis.sadd (toIdentifier ref) (map toBS vals))
    >>= expectRight "setInsert"
    >>= ignore @Integer

-- | Insert into a Redis set in a transaction.
txSInsert :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> [a] -> Tx (RefInstance ref) ()
txSInsert ref vals =
  void . txWrap
    $ Hedis.sadd (toIdentifier ref) (map toBS vals)

-- | Delete from a Redis set.
sDelete :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> [a] -> RedisM (RefInstance ref) ()
sDelete ref vals =
  Redis (Hedis.srem (toIdentifier ref) (map toBS vals))
    >>= expectRight "hashSetDelete"
    >>= ignore @Integer

-- | Delete from a Redis set in a transaction.
txSDelete :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> [a] -> Tx (RefInstance ref) ()
txSDelete ref vals =
  void . txWrap
    $ Hedis.srem (toIdentifier ref) (map toBS vals)

-- | Check membership in a Redis set.
sContains :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> a -> RedisM (RefInstance ref) Bool
sContains ref val =
  Redis (Hedis.sismember (toIdentifier ref) (toBS val))
    >>= expectRight "setContains"

-- | Check membership in a Redis set, in a transaction.
txSContains :: forall ref a. (Ref ref, ValueType ref ~ Set a, Serializable a) => ref -> a -> Tx (RefInstance ref) Bool
txSContains ref val =
  txWrap $ Hedis.sismember (toIdentifier ref) (toBS val)

-- | Get set size.
sSize :: (Ref ref, ValueType ref ~ Set a) => ref -> RedisM (RefInstance ref) Integer
sSize ref = Redis (Hedis.scard (toIdentifier ref)) >>= expectRight "setSize"

-- | Get set size, in a transaction.
txSSize :: (Ref ref, ValueType ref ~ Set a) => ref -> Tx (RefInstance ref) Integer
txSSize ref = txWrap $ Hedis.scard (toIdentifier ref)

-- | Priority for a sorted set
newtype Priority = Priority { unPriority :: Double }

instance Serializable Priority where
  fromBS = fmap Priority . fromBS
  toBS   = toBS . unPriority

instance Bounded Priority where
  minBound = Priority (-Numeric.Limits.maxValue)
  maxBound = Priority   Numeric.Limits.maxValue

-- | Add elements to a sorted set
zInsert :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a) => ref -> [(Priority, a)] -> RedisM (RefInstance ref) ()
zInsert (toIdentifier -> keyBS) vals =
  Redis (Hedis.zadd keyBS (map (unPriority Arrow.*** toBS) vals))
    >>= expectRight "zadd"
    >>= ignore @Integer

-- | Delete from a Redis sorted set
zDelete :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a) => ref -> a -> RedisM (RefInstance ref) ()
zDelete (toIdentifier -> keyBS) val =
  Redis (Hedis.zrem keyBS [toBS val])
    >>= expectRight "zrem"
    >>= ignore @Integer

-- | Get the cardinality (number of elements) of a sorted set
zSize :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a) => ref -> RedisM (RefInstance ref) Integer
zSize (toIdentifier -> keyBS) =
  Redis (Hedis.zcard keyBS)
    >>= expectRight "zcard"

-- | Returns the number of elements in the sorted set that have a score between minScore and
-- maxScore inclusive.
zCount :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a) => ref -> Priority -> Priority -> RedisM (RefInstance ref) Integer
zCount (toIdentifier -> keyBS) (unPriority -> minScore) (unPriority -> maxScore) =
  Redis (Hedis.zcount keyBS minScore maxScore)
    >>= expectRight "zcount"

-- | Remove given number of smallest elements from a sorted set.
--   Available since Redis 5.0.0
zPopMin :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a) => ref -> Integer -> RedisM (RefInstance ref) [(Priority, a)]
zPopMin (toIdentifier -> keyBS) cnt =
  Redis (zpopmin keyBS cnt)
  >>= expectRight "zpopmin call"
  >>= expectRight "zpopmin decode" . fromBSMany'
  where fromBSMany' = traverse $ \(valBS,sc) -> maybe (Left valBS) (Right . (Priority sc,)) $ fromBS valBS

-- | ZPOPMIN as it should be in the Hedis library (but it isn't yet)
--   Available since Redis 5.0.0
zpopmin :: Hedis.RedisCtx m f => ByteString -> Integer -> m (f [(ByteString, Double)])
zpopmin k c = Hedis.sendRequest ["ZPOPMIN", k, toBS c]

-- | Remove the smallest element from a sorted set, and block for the given number of seconds when it is not there yet.
--   Available since Redis 5.0.0
bzPopMin :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a)
         => ref -> Integer -> RedisM (RefInstance ref) (Maybe (Priority, a))
bzPopMin (toIdentifier -> keyBS) timeout =
  Redis (bzpopmin keyBS timeout)
  >>= expectRight "bzPopMin call"
  >>= expectRight "bzPopMin decode" . fromBS'
  where
    fromBS' = maybe (Right Nothing) (\(_,valBS,sc) -> maybe (Left valBS) (Right . Just . (Priority sc,)) $ fromBS valBS)

-- | BZPOPMIN as it should be in the Hedis library (but it isn't yet)
--   Available since Redis 5.0.0
bzpopmin :: Hedis.RedisCtx m f => ByteString -> Integer -> m (f (Maybe (ByteString, ByteString, Double)))
bzpopmin k timeout = Hedis.sendRequest ["BZPOPMIN", k, toBS timeout]

-- Orphan instance, Hedis only implements this for 2-tuples, but BZPOPMIN gets 3 results
instance (Hedis.RedisResult a, Hedis.RedisResult b, Hedis.RedisResult c) => Hedis.RedisResult (a,b,c) where
  decode (Hedis.MultiBulk (Just [x,y,z])) = (,,) <$> Hedis.decode x <*> Hedis.decode y <*> Hedis.decode z
  decode r                                = Left r

-- | Get elements from a sorted set, between the given min and max values, and with the given offset and limit.
zRangeByScoreLimit :: forall ref a. (Ref ref, ValueType ref ~ [(Priority, a)], Serializable a)
                   => ref -> Priority -> Priority -> Integer -> Integer -> RedisM (RefInstance ref) [a]
zRangeByScoreLimit (toIdentifier -> keyBS) (Priority minV) (Priority maxV) offset limit =
  Hedis.zrangebyscoreLimit keyBS minV maxV offset limit
  >>= expectRight "zrangebyscoreLimit call"
  >>= expectRight "zrangebyscoreLimit decode" . fromBSMany

parseMap :: (Ord k, Serializable k, Serializable v)
  => [(ByteString, ByteString)] -> Maybe (Map k v)
parseMap kvsBS = Map.fromList <$> sequence
  [ (,) <$> fromBS keyBS <*> fromBS valBS
  | (keyBS, valBS) <- kvsBS
  ]

-- | Redis hashes.
instance (Ord k, Serializable k, Serializable v) => Value inst (Map k v) where
  type Identifier (Map k v) = ByteString

  txValGet keyBS =
    txWrap (Hedis.hgetall keyBS)
      & txCheckMap (
          maybe
            (Left $ CouldNotDecodeValue Nothing)
            (Right . Just)
          . parseMap
        )

  txValSet keyBS m =
    void $ txWrap (
      Hedis.del [keyBS]
      *> Hedis.hmset keyBS
        [(toBS ref, toBS val) | (ref, val) <- Map.toList m]
    )

  txValDelete keyBS = void . txWrap $ Hedis.del [keyBS]
  txValSetTTLIfExists keyBS (TTLSec ttlSec) =
    txWrap $ Hedis.expire keyBS ttlSec

  valGet keyBS =
    Hedis.hgetall keyBS
      >>= expectRight "valGet/Map k v"
      >>= \kvsBS -> case parseMap kvsBS of
        Just m -> pure (Just m)
        Nothing -> throw $ CouldNotDecodeValue Nothing

  valSet keyBS m =
    Redis (Hedis.multiExec (
      Hedis.del [keyBS]
      *> Hedis.hmset keyBS
        [(toBS ref, toBS val) | (ref, val) <- Map.toList m]
    ))
      >>= expectTxSuccess
      >>= expect "valSet/Map k v" Hedis.Ok

  valDelete keyBS =
    Redis (Hedis.del [keyBS])
      >>= expectRight "valDelete/Map k v"
      >>= ignore @Integer

  valSetTTLIfExists keyBS (TTLSec ttlSec) =
    Redis (Hedis.expire keyBS ttlSec)
      >>= expectRight "setTTLIfExists/Map k v"

infix 3 :/
-- | Map field addressing operator.
-- If @ref@ is a 'Ref' pointing to a @Map k v@,
-- then @(ref :/ k)@ is a ref with type @v@,
-- pointing to the entry in the map identified by @k@.
data MapItem :: Type -> Type -> Type -> Type where
  (:/) :: (Ref ref, ValueType ref ~ Map k v) => ref -> k -> MapItem ref k v

  -- Previously, 'MapItem' was defined simply as
  -- > data MapItem ref k v = (:/) ref k
  -- However, this caused GHC to choke on this because it provided no way
  -- to infer the value of 'v' from @ref :/ k@ alone -- 'v' is a phantom type,
  -- not mentioned in the expression.
  --
  -- This would block the instance resolution for @Ref (MapItem ref k v)@
  -- for any expression of the form @ref :/ k@, and cause more trouble down the line.
  --
  -- Hence I made 'MapItem' a GADT so that the type inference machine
  -- has clear instructions how to infer the correct value of 'v'.

instance
  ( Ref ref
  , ValueType ref ~ Map k v
  , Serializable k
  , SimpleValue (RefInstance ref) v
  ) => Ref (MapItem ref k v) where

  type ValueType (MapItem ref k v) = v
  type RefInstance (MapItem ref k v) = RefInstance ref
  toIdentifier (mapRef :/ k) = SviHash (toIdentifier mapRef) (toBS k)

infix 3 :.
-- | Record item addressing operator.
-- If @ref@ is a ref pointing to a @Record fieldF@,
-- and @k :: fieldF v@ is a field of that record,
-- then @(ref :. k)@ is a ref with type @v@,
-- pointing to that field of that record.
data RecordItem ref fieldF val = (:.) ref (fieldF val)

-- | Class of record fields. See 'Record' for details.
class RecordField (fieldF :: Type -> Type) where
  rfToBS :: fieldF a -> ByteString

instance
  ( Ref ref
  , ValueType ref ~ Record fieldF
  , SimpleValue (RefInstance ref) val
  , RecordField fieldF
  ) => Ref (RecordItem ref fieldF val) where

  type ValueType (RecordItem ref fieldF val) = val
  type RefInstance (RecordItem ref fieldF val) = RefInstance ref
  toIdentifier (ref :. field) = SviHash (toIdentifier ref) (rfToBS field)

-- | The value type for refs that point to records.
-- Can be deleted and SetTTLed.
-- Can't be read or written as a whole (at the moment).
--
-- The parameter @fieldF@ gives the field functor for this record.
-- This is usually a GADT indexed by the type of the corresponding record field.
--
-- 'Record' and 'Map' are related but different:
--
-- * 'Map' is a homogeneous variable-size collection of associations @k -> v@,
--   where all refs have the same type and all values have the same type,
--   just like a Haskell 'Map'.
--
--   'Map's can be read/written to Redis as whole entities out-of-the-box.
--
-- * 'Record' is a heterogeneous fixed-size record of items with different types,
--   just like Haskell records.
--
--   'Record's cannot be read/written whole at the moment.
--   There's no special reason for that, except that it would probably be
--   too much type-level code that noone needs at the moment.
--
--  See also: '(:.)'.
data Record (fieldF :: Type -> Type)

-- This is a bit of a hack. Records can't be written at the moment.
-- Maybe we should split the Value typeclass into ReadWriteValue and Value
instance Value inst (Record fieldF) where
  type Identifier (Record fieldF) = ByteString
  txValGet _ = error "Record is not meant to be read"
  txValSet _ _ = error "Record is not meant to be written"
  txValDelete keyBS = void . txWrap $ Hedis.del [keyBS]
  txValSetTTLIfExists keyBS (TTLSec ttlSec) = txWrap $ Hedis.expire keyBS ttlSec
  valGet _ = error "Record is not meant to be read"
  valSet _ _ = error "Record is not meant to be written"
  valDelete keyBS = Hedis.del [keyBS]
    >>= expectRight "valDelete/Record" >>= ignore @Integer
  valSetTTLIfExists keyBS (TTLSec ttlSec) =
    Hedis.expire keyBS ttlSec >>= expectRight "setTTLIfExists/Record"

unliftIO :: ((forall a. RedisM inst a -> IO a) -> IO b) -> RedisM inst b
unliftIO action = Redis $ Hedis.reRedis $ do
  env <- ask
  liftIO $ action $
    \(Redis redisA) -> runReaderT (Hedis.unRedis redisA) env

-- | PubSub channels.
data PubSub msg

instance Value inst (PubSub msg) where
  type Identifier (PubSub msg) = ByteString
  txValGet _ = error "PubSub is not meant to be read"
  txValSet _ _ = error "PubSub is not meant to be written"
  txValDelete keyBS = void . txWrap $ Hedis.del [keyBS]
  txValSetTTLIfExists keyBS (TTLSec ttlSec) = txWrap $ Hedis.expire keyBS ttlSec
  valGet _ = error "PubSub is not meant to be read"
  valSet _ _ = error "PubSub is not meant to be written"
  valDelete keyBS = Hedis.del [keyBS]
    >>= expectRight "valDelete/PubSub" >>= ignore @Integer
  valSetTTLIfExists keyBS (TTLSec ttlSec) =
    Hedis.expire keyBS ttlSec >>= expectRight "setTTLIfExists/PubSub"

pubSubListen :: (Ref ref, ValueType ref ~ PubSub msg, Serializable msg)
  => ref -> (Either RedisException msg -> IO Bool) -> RedisM (RefInstance ref) ()
pubSubListen (toIdentifier -> keyBS) process =
  Redis $ Hedis.pubSub (Hedis.subscribe [keyBS]) $ \rawMsg ->
    let msg = case fromBS (Hedis.msgMessage rawMsg) of
          Nothing -> Left (CouldNotDecodeValue $ Just (Hedis.msgMessage rawMsg))
          Just msg' -> Right msg'
    in liftIO (process msg) >>= \case
      True -> return mempty
      False -> return (Hedis.unsubscribe [keyBS])

pubSubCountSubs :: (Ref ref, ValueType ref ~ PubSub msg)
  => ref -> RedisM (RefInstance ref) Integer
pubSubCountSubs (toIdentifier -> keyBS) =
  Hedis.sendRequest ["PUBSUB", "NUMSUB", keyBS]
    >>= expectRight "pubSubCountSubs" 
    >>= \case
      Hedis.MultiBulk (Just [_, Hedis.Integer cnt]) -> return cnt
      _ -> error "pubSubCountSubs: unexpected reply"