keyed-vals-redis-0.2.3.6: src/KeyedVals/Handle/Redis/Internal.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune not-home #-}
{- |
Copyright : (c) 2018-2022 Tim Emiola
SPDX-License-Identifier: BSD3
Maintainer : Tim Emiola <tim@emio.la>
Provides a 'Handle' that stores data in Redis.
-}
module KeyedVals.Handle.Redis.Internal (
-- * Handle creation
fromConnectInfo,
-- * Configuration
readEnvConnectInfo,
-- * module re-exports
module KeyedVals.Handle,
) where
import Control.Exception (throwIO)
import Control.Monad.IO.Unlift (MonadIO, MonadUnliftIO, liftIO)
import qualified Data.ByteString as B
import Data.Functor ((<&>))
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Maybe (mapMaybe)
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8)
import Database.Redis (
ConnectInfo (..),
Connection,
Redis,
Reply (..),
Status (..),
checkedConnect,
del,
disconnect,
get,
hdel,
hget,
hgetall,
hkeys,
hlen,
hmget,
hmset,
hset,
keys,
parseConnectInfo,
runRedis,
set,
)
import KeyedVals.Handle
import KeyedVals.Handle.Internal (Handle (..))
import Numeric.Natural (Natural)
import System.ReadEnvVar (lookupEnv, readEnvDef)
-- | Determine 'ConnectInfo' from the environment.
readEnvConnectInfo :: IO (Maybe ConnectInfo)
readEnvConnectInfo = do
maxConns <- readEnvDef "REDIS_MAX_CONNECTIONS" fallbackMaxConns
lookupEnv "REDIS_URL" >>= maybe (pure Nothing) (parseLocator maxConns)
-- | Obtain a @ConnectInfo@ from a Redis Url and max connections
parseLocator :: Int -> String -> IO (Maybe ConnectInfo)
parseLocator maxConns l = do
let parseConnectInfo' = either invalidLocator pure . parseConnectInfo
setMaxConns x cfg = cfg {connectMaxConnections = x}
Just . setMaxConns maxConns <$> parseConnectInfo' l
invalidLocator :: String -> IO a
invalidLocator x = throwIO $ userError $ "REDIS connection url: " ++ x ++ " is invalid"
-- | Create a 'Handle'.
fromConnectInfo :: (MonadUnliftIO m) => ConnectInfo -> m (Handle m)
fromConnectInfo connectInfo = do
conn <- liftIO $ checkedConnect connectInfo
pure $
Handle
{ hClose = hClose' conn
, hLoadVal = hLoadVal' conn
, hSaveVal = hSaveVal' conn
, hLoadKVs = hLoadKVs' conn
, hSaveKVs = hSaveKVs' conn
, hUpdateKVs = hUpdateKVs' conn
, hLoadFrom = hLoadFrom' conn
, hLoadSlice = hLoadSlice' conn
, hSaveTo = hSaveTo' conn
, hDeleteSelected = hDeleteSelected' conn
, hDeleteSelectedKVs = hDeleteSelectedKVs' conn
, hCountKVs = hCountKVs' conn
}
hClose' :: (MonadUnliftIO m) => Connection -> m ()
hClose' = liftIO . disconnect
hLoadVal' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
m (Either HandleErr (Maybe Val))
hLoadVal' conn key = doFetch conn $ get key
hSaveVal' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
Val ->
m (Either HandleErr ())
hSaveVal' conn key value = doStore conn $ set key value
hLoadFrom' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
Key ->
m (Either HandleErr (Maybe Val))
hLoadFrom' conn key dictKey = doFetch conn $ hget key dictKey
hLoadKVs' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
m (Either HandleErr ValsByKey)
hLoadKVs' conn key = doFetch conn $ hgetall key <&> fmap Map.fromList
hLoadSlice' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
Selection ->
m (Either HandleErr ValsByKey)
hLoadSlice' conn key m@(Match _) = selectKeysThen hLoadSlice' conn key m
hLoadSlice' conn key (AllOf dictKeys) = do
doFetch conn (hmget key dictKeys) >>= \case
Left err -> pure $ Left err
Right fetched -> do
let pairedMaybes = zip (NonEmpty.toList dictKeys) fetched
mbOf (x, mbY) = mbY >>= \y -> Just (x, y)
pure $ Right $ Map.fromList $ mapMaybe mbOf pairedMaybes
hCountKVs' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
m (Either HandleErr Natural)
hCountKVs' conn key = doFetch conn $ hlen key <&> fmap fromInteger
hSaveTo' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
Key ->
Val ->
m (Either HandleErr ())
hSaveTo' conn key dictKey value = doStore' conn $ hset key (NonEmpty.singleton (dictKey, value))
hSaveKVs' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
ValsByKey ->
m (Either HandleErr ())
hSaveKVs' conn key dict = do
_ <- hDeleteSelected' conn $ AllOf (key :| [])
hUpdateKVs' conn key dict
hUpdateKVs' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
ValsByKey ->
m (Either HandleErr ())
hUpdateKVs' conn key dict =
case NonEmpty.nonEmpty $ Map.toList dict of
Just x -> doStore' conn $ hmset key x
Nothing -> pure $ Right ()
hDeleteSelected' ::
(MonadUnliftIO m) =>
Connection ->
Selection ->
m (Either HandleErr ())
hDeleteSelected' conn (AllOf ks) = doStore' conn $ del ks
hDeleteSelected' conn (Match g) = do
doFetch conn (keys $ globPattern g) >>= \case
Left e -> pure $ Left e
Right [] -> pure $ Right ()
Right (k : ks) -> hDeleteSelected' conn $ AllOf (k :| ks)
hDeleteSelectedKVs' ::
(MonadUnliftIO m) =>
Connection ->
Key ->
Selection ->
m (Either HandleErr ())
hDeleteSelectedKVs' conn key (AllOf dictKeys) = doStore' conn $ hdel key dictKeys
hDeleteSelectedKVs' conn key m@(Match _) = selectKeysThen hDeleteSelectedKVs' conn key m
selectKeysThen ::
(Monoid b, MonadUnliftIO m) =>
(Connection -> B.ByteString -> Selection -> m (Either HandleErr b)) ->
Connection ->
B.ByteString ->
Selection ->
m (Either HandleErr b)
selectKeysThen f conn key selection = do
doFetch conn (hkeys key) >>= \case
Left e -> pure $ Left e
Right [] -> pure $ Right mempty
Right xs -> do
case filter (`isIn` selection) xs of
[] -> pure $ Right mempty
(k : ks) -> f conn key $ AllOf (k :| ks)
fallbackMaxConns :: Int
fallbackMaxConns = 10
toHandleErr :: Reply -> HandleErr
toHandleErr (Error e) | B.isPrefixOf "WRONGTYPE" e = BadKey
toHandleErr (Error e) = Unanticipated $ decodeUtf8 e
toHandleErr r = Unanticipated $ Text.pack $ show r
doStore ::
(MonadIO m) =>
Connection ->
Redis (Either Reply Status) ->
m (Either HandleErr ())
doStore conn action = liftIO $ leftErr $ runRedis conn action
leftErr :: (Monad m) => m (Either Reply Status) -> m (Either HandleErr ())
leftErr x =
x >>= \case
(Left l) -> pure $ Left $ toHandleErr l
Right Ok -> pure $ Right ()
Right Pong -> pure $ Right ()
Right (Status err) -> pure $ Left $ Unanticipated $ Text.pack $ show err
doStore' ::
(MonadIO m) =>
Connection ->
Redis (Either Reply a) ->
m (Either HandleErr ())
doStore' conn action = liftIO $ leftErr'' $ runRedis conn action
leftErr'' :: (Monad m) => m (Either Reply a) -> m (Either HandleErr ())
leftErr'' x =
x >>= \case
(Left l) -> pure $ Left $ toHandleErr l
Right _ -> pure $ Right ()
doFetch ::
(MonadUnliftIO m) =>
Connection ->
Redis (Either Reply a) ->
m (Either HandleErr a)
doFetch conn = liftIO . leftErr' . runRedis conn
leftErr' :: (Monad m) => m (Either Reply a) -> m (Either HandleErr a)
leftErr' = (<&> either (Left . toHandleErr) Right)