Add a simple logging effect and adapt only at IO sites

This commit is contained in:
James Eversole 2024-10-12 17:38:22 -05:00
parent 6e2fb3b9bd
commit 9a8bd089e5
4 changed files with 76 additions and 36 deletions

View File

@ -7,3 +7,5 @@ A nix starting template for web projects utilizing
- [Lucid](https://github.com/chrisdone/lucid)
- [PostgreSQL](https://www.postgresql.org/)
- [Servant](https://github.com/haskell-servant/servant)
The repository has a simple CRUD implementation of a "Users" API which demonstrates how to use included effects, create your own effects, and bubble errors to Servant's `ServerError` type.

View File

@ -1,21 +1,25 @@
module Core where
import Control.Exception (IOException)
import Control.Monad.Catch (catch)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Text as T
import Data.ByteString.Lazy.UTF8 (fromString)
import Database.PostgreSQL.Simple (Query)
import Database.PostgreSQL.Simple.FromRow (FromRow, field, fromRow)
import Database.PostgreSQL.Simple.ToRow (ToRow, toRow)
import Database.PostgreSQL.Simple.ToField (ToField)
import Database.PostgreSQL.Simple.ToRow (ToRow, toRow)
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Error.Static (Error)
import Effectful.Error.Static (Error, throwError)
import Effectful.FileSystem
import GHC.Generics (Generic)
import Servant hiding ((:>))
import Servant hiding ((:>), throwError)
import qualified Servant as S
--
-- Core data
--
data User = User { userId :: Int, userName :: String}
deriving (Show, Generic)
@ -27,22 +31,55 @@ instance ToRow User where
instance ToJSON User
instance FromJSON User
--
-- Effects
type AppEff = Eff '[Database, FileSystem, Error ServerError, IOE]
--
type AppEff = Eff '[Database, FileSystem, Logger, Error ServerError, IOE]
-- Database
data Database :: Effect where
DatabaseRead :: (Query, Int) -> Database (Eff es) (Maybe User)
DatabaseWrite :: (Query, String) -> Database (Eff es) ()
type instance DispatchOf Database = 'Dynamic
type DatabaseEffects es = (IOE :> es, Logger :> es, Error ServerError :> es)
databaseRead :: (Database :> es, Error ServerError :> es) => (Query, Int) -> Eff es (Maybe User)
databaseRead = send . DatabaseRead
databaseWrite :: (Database :> es, Error ServerError :> es) => (Query, String) -> Eff es ()
databaseWrite = send . DatabaseWrite
-- Logger
data Logger :: Effect where
WriteLog :: String -> Logger (Eff es) ()
type instance DispatchOf Logger = 'Dynamic
writeLog :: (Logger :> es, Error ServerError :> es) => String -> Eff es ()
writeLog = send . WriteLog
runLoggerIO :: (IOE :> es, Error ServerError :> es) => Eff (Logger : es) a -> Eff es a
runLoggerIO = interpret $ \_ -> \case
WriteLog msg -> adapt $ log msg
where
log :: String -> IO ()
log msg = putStrLn msg
-- Utility
adapt :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a
adapt m = liftIO m `catch` \(e::IOException) ->
throwError $ ServerError
{ errHTTPCode = 500
, errReasonPhrase = "Internal Database Error"
, errBody = fromString $ show e
, errHeaders = []
}
--
-- Routes
--
type Root = Get '[PlainText] T.Text
type UserList = "user" S.:> Get '[JSON] [User]
type UserGet = "user" S.:> Capture "userId" Int S.:> Get '[JSON] User

View File

@ -3,53 +3,45 @@ module Database where
import Core
import Control.Exception (IOException)
import Control.Monad.Catch (catch)
import Data.Aeson (ToJSON)
import Data.ByteString.Lazy.UTF8 (fromString)
import Data.Maybe (listToMaybe)
import Database.PostgreSQL.Simple
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Error.Static (Error, HasCallStack, catchError, runErrorNoCallStack, throwError)
import Effectful.Error.Static (Error)
import Servant hiding ((:>), throwError)
runDatabaseDebug :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a
runDatabaseDebug :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
runDatabaseDebug = interpret $ \_ -> \case
DatabaseRead (statement, values) -> adapt $ read statement values
DatabaseWrite (statement, values) -> adapt $ write statement values
DatabaseRead (statement, values) -> read statement values
DatabaseWrite (statement, values) -> write statement values
where
read _ values =
putStrLn "We just mocked a READ database operation" >>= \_ -> pure $
writeLog "We just mocked a READ database operation" >>= \_ -> pure $
Just (User values "Mock User")
write _ values =
putStrLn $ "We just mocked a WRITE database operation with a user named "
writeLog $ "We just mocked a WRITE database operation with a user named "
++ values
runDatabaseIO :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a
runDatabaseIO :: DatabaseEffects es => Eff (Database : es) a -> Eff es a
runDatabaseIO = interpret $ \_ -> \case
DatabaseRead (statement, values) -> adapt $ read statement values
DatabaseWrite (statement, values) -> adapt $ write statement values
DatabaseRead (statement, values) -> read statement values
DatabaseWrite (statement, values) -> write statement values
where
read :: Query -> Int -> IO (Maybe User)
read statement values = do
conn <- openConn
user <- query conn statement (Only values)
pure $ listToMaybe user
write :: Query -> String -> IO ()
write statement values = do
conn <- openConn
execute conn statement (Only values)
putStrLn $ "Wrote user to database using statement:\n" ++ show statement
openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10"
read :: DatabaseEffects es => Query -> Int -> Eff es (Maybe User)
read statement values = do
conn <- adapt $ openConn
users <- adapt $ query conn statement (Only values)
pure $ listToMaybe users
adapt :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a
adapt m = liftIO m `catch` \(e::IOException) ->
throwError $ ServerError
{ errHTTPCode = 500
, errReasonPhrase = "Internal Database Error"
, errBody = fromString $ show e
, errHeaders = []
}
write :: DatabaseEffects es => Query -> String -> Eff es ()
write statement values = do
conn <- adapt openConn
adapt $ execute conn statement (Only values)
writeLog $ "Wrote user to database using statement:\n" ++ show statement
openConn :: IO Connection
openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10"
queryUser :: Int -> (Query, Int)
queryUser userId = ("SELECT id, name FROM users WHERE id = ?;", userId)

View File

@ -13,6 +13,9 @@ import Network.Wai.Handler.Warp (run)
import Servant hiding ((:>), throwError)
import qualified Servant as S
--
-- Application
--
main :: IO ()
main = run port $ serve proxy app
@ -23,8 +26,14 @@ app = α $ rootHandler
:<|> userPostHandler
α :: ServerT API AppEff -> ServerT API Handler
α = hoistServer proxy $ Handler . ExceptT .
runEff . runErrorNoCallStack . runFileSystem . runDatabaseIO
α = hoistServer proxy
$ Handler
. ExceptT
. runEff
. runErrorNoCallStack
. runLoggerIO
. runFileSystem
. runDatabaseIO
port :: Int
port = 8080