{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -- | DSL/interpreter model for a generic key-value database module Imm.Database where -- {{{ Imports import Imm.Error import Imm.Logger import Imm.Prelude import Control.Monad.Trans.Free import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>)) -- }}} -- * DSL/interpreter -- | Generic database table class (Ord (Key t), Show (Key t), Show (Entry t), Typeable t, Show t, Pretty t, Pretty (Key t), Pretty (Entry t)) => Table t where type Key t :: * type Entry t :: * -- | Database DSL data DatabaseF t next = Describe t (Doc -> next) | FetchList t [Key t] (Either SomeException (Map (Key t) (Entry t)) -> next) | FetchAll t (Either SomeException (Map (Key t) (Entry t)) -> next) | Update t (Key t) (Entry t -> Entry t) (Either SomeException () -> next) | InsertList t [(Key t, Entry t)] (Either SomeException () -> next) | DeleteList t [Key t] (Either SomeException () -> next) | Purge t (Either SomeException () -> next) | Commit t (Either SomeException () -> next) deriving(Functor) -- | Database interpreter data CoDatabaseF t m a = CoDatabaseF { describeH :: m (Doc, a) , fetchListH :: [Key t] -> m (Either SomeException (Map (Key t) (Entry t)), a) , fetchAllH :: m (Either SomeException (Map (Key t) (Entry t)), a) , updateH :: Key t -> (Entry t -> Entry t) -> m (Either SomeException (), a) , insertListH :: [(Key t, Entry t)] -> m (Either SomeException (), a) , deleteListH :: [Key t] -> m (Either SomeException (), a) , purgeH :: m (Either SomeException (), a) , commitH :: m (Either SomeException (), a) } deriving(Functor) instance Monad m => PairingM (CoDatabaseF t m) (DatabaseF t) m where -- pairM :: (a -> b -> m r) -> f a -> g b -> m r pairM p CoDatabaseF{describeH} (Describe _ next) = do (result, a) <- describeH p a $ next result pairM p CoDatabaseF{fetchListH} (FetchList _ key next) = do (result, a) <- fetchListH key p a $ next result pairM p CoDatabaseF{fetchAllH} (FetchAll _ next) = do (result, a) <- fetchAllH p a $ next result pairM p CoDatabaseF{updateH} (Update _ key f next) = do (result, a) <- updateH key f p a $ next result pairM p CoDatabaseF{insertListH} (InsertList _ rows next) = do (result, a) <- insertListH rows p a $ next result pairM p CoDatabaseF{deleteListH} (DeleteList _ k next) = do (result, a) <- deleteListH k p a $ next result pairM p CoDatabaseF{purgeH} (Purge _ next) = do (result, a) <- purgeH p a $ next result pairM p CoDatabaseF{commitH} (Commit _ next) = do (result, a) <- commitH p a $ next result -- * Exception data DatabaseException t = NotCommitted t | NotDeleted t [Key t] | NotFound t [Key t] | NotInserted t [(Key t, Entry t)] | NotPurged t | NotUpdated t (Key t) | UnableFetchAll t deriving instance (Eq t, Eq (Key t), Eq (Entry t)) => Eq (DatabaseException t) deriving instance (Show t, Show (Key t), Show (Entry t)) => Show (DatabaseException t) instance (Table t, Show (Key t), Show (Entry t), Pretty (Key t), Typeable t) => Exception (DatabaseException t) where displayException = show . pretty instance (Pretty t, Pretty (Key t)) => Pretty (DatabaseException t) where pretty (NotCommitted _) = text "Unable to commit database changes." pretty (NotDeleted _ x) = text "Unable to delete the following entries in database:" <++> indent 2 (vsep $ map pretty x) pretty (NotFound _ x) = text "Unable to find the following entries in database:" <++> indent 2 (vsep $ map pretty x) pretty (NotInserted _ x) = text "Unable to insert the following entries in database:" <++> indent 2 (vsep $ map (pretty . fst) x) pretty (NotPurged t) = text "Unable to purge database" <+> pretty t pretty (NotUpdated _ x) = text "Unable to update the following entry in database:" <++> indent 2 (pretty x) pretty (UnableFetchAll _) = text "Unable to fetch all entries from database." -- * Primitives describeDatabase :: (MonadFree f m, DatabaseF t :<: f) => t -> m Doc describeDatabase t = liftF . inj $ Describe t id fetch :: (MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m) => t -> Key t -> m (Entry t) fetch t k = do results <- liftF . inj $ FetchList t [k] id result <- lookup k <$> liftE results maybe (throwM $ NotFound t [k]) return result fetchList :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t)) fetchList t k = do result <- liftF . inj $ FetchList t k id liftE result fetchAll :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t)) fetchAll t = do result <- liftF . inj $ FetchAll t id liftE result update :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m () update t k f = do result <- liftF . inj $ Update t k f id liftE result insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> Entry t -> m () insert t k v = insertList t [(k, v)] insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [(Key t, Entry t)] -> m () insertList t i = do logInfo $ "Inserting " <> yellow (pretty $ length i) <> " entries..." result <- liftF . inj $ InsertList t i id liftE result delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m () delete t k = deleteList t [k] deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [Key t] -> m () deleteList t k = do logInfo $ "Deleting " <> yellow (pretty $ length k) <> " entries..." result <- liftF . inj $ DeleteList t k id liftE result purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m () purge t = do logInfo "Purging database..." result <- liftF . inj $ Purge t id liftE result commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m () commit t = do logDebug "Committing database transaction..." result <- liftF . inj $ Commit t id liftE result logDebug "Database transaction committed"