Safe Haskell | None |
---|---|
Language | Haskell2010 |
Micro-framework for building a non-web application
This is a version of the ReaderT Design Pattern.
https://www.fpcomplete.com/blog/2017/06/readert-design-pattern
Basic Usage
Start by defining a type to hold your global application state:
data App = App { appDryRun :: Bool , appLogLevel :: LogLevel }
This type can be as complex or simple as you want. It might hold a separate
Config
attribute or may keep everything as one level of properties. It
could even hold an
if you need mutable application state.IORef
The only requirements are some notion of a
:LogLevel
instance HasLogging App where getLogLevel = appLogLevel getLogFormat _ = FormatTerminal getLogLocation _ = LogStdout
and a way to build a value:
loadApp :: IO App
It's likely you'll want to use Freckle.App.Env
to load your App
:
import qualified Freckle.App.Env as Env loadApp = Env.parse $ App <$> Env.switch "DRY_RUN" mempty <*> Env.flag LevelInfo LevelDebug "DEBUG" mempty
Though not required, a type synonym can make things throughout your application a bit more readable:
type AppM = ReaderT App (LoggingT IO)
Now you have application-specific actions that can do IO, log, and access your state:
myAppAction :: AppM () myAppAction = do isDryRun <- asks appDryRun if isDryRun then $logWarn "Skipping due to dry-run" else liftIO $ fireTheMissles
These actions can be (composed of course, or) invoked by a main
that
handles the reader context and evaluating the logging action.
main :: IO () main = do app <- loadApp runApp app $ do myAppAction myOtherAppAction
Database
Adding Database access requires an instance of
on your HasSqlPool
App
type. Most often, this will be easiest if you indeed separate a Config
attribute:
data Config = Config { configDbPoolSize :: Int , configLogLevel :: LogLevel }
So you can isolate Env-related concerns
loadConfig :: IO Config loadConfig = Env.parse $ Config <$> Env.var Env.auto "PGPOOLSIZE" (Env.def 1) <*> Env.flag LevelInfo LevelDebug "DEBUG" mempty
from the runtime application state:
data App = App { appConfig :: Config , appSqlPool :: SqlPool } instance HasLogging App where getLogLevel = configLogLevel . appConfig getLogFormat _ = FormatTerminal getLogLocation _ = LogStdout
The Freckle.App.Database
module provides
for
building a Pool given this (limited) config data:makePostgresPool
loadApp :: IO App loadApp = do appConfig{..} <- loadConfig appSqlPool <- makePostgresPool configDbPoolSize pure App{..} instance HasSqlPool App where getSqlPool = appSqlPool
Note: the actual database connection parameters (host, user, etc) are (currently) parsed from conventional environment variables by the underlying driver directly. Our application configuration is only involved in declaring the pool size.
This unlocks
for your application:runDB
myAppAction :: AppM [Entity Something] myAppAction = runDB $ selectList [] []
Testing
Freckle.App.Test
exposes an
type for examples in a
AppExample
spec. The can be run by giving your SpecWith
ApploadApp
function to
Hspec's
.before
Our Test
module also exposes
for running runAppTest
AppM
actions and
lifted expectations for use within such an example.
spec :: Spec spec = before loadApp $ do describe "myAppAction" $ do it "works" $ do result <- runAppTest myAppAction result `shouldBe` "as expected"
If your application makes use of the database, a few things will have to be different:
First, we want to have a specialized
in tests to avoid excessive
annotations because of the generalized type of runDB
itself:runDB
import Database.Persist.Sql import qualified Freckle.App.Database as DB runDB :: SqlPersistM IO a -> AppExample App a runDB = DB.runDB
Second, we'll probably want a conventional withApp
function so that we can
truncate tables as part of spec setup:
import Freckle.App (runApp) import Freckle.App.Test hiding (withApp) import Test.Hspec withApp :: SpecWith App -> Spec withApp = before $ do app <- loadApp runSqlPool truncateTables $ appSqlPool app pure app
And now you can write specs that also use the database:
spec :: Spec spec = withApp $ do describe "myAppAction" $ do it "works" . withGraph runDB do nodeWith -- ... nodeWith -- ... nodeWith -- ... result <- lift $ runAppTest myAppAction result `shouldBe` "as expected"
Documentation
module Freckle.App.Logging
module Freckle.App.Database
module Control.Monad.Reader
module Control.Monad.Logger