| Safe Haskell | Safe-Inferred |
|---|---|
| Language | Haskell2010 |
Freckle.App
Description
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
, appLogger :: Logger
}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 HasLogger:
instance HasLogger App where
loggerL = lens appLogger $ \x y -> x { appLogger = y }and a bracketed function for building and using a value:
loadApp :: (App -> m a) -> m a loadApp f = do app <- -- ... f app
It's likely you'll want to use Freckle.App.Env to load your App:
import qualified Blammo.Logger.LogSettings.Env as LoggerEnv
import qualified Freckle.App.Env as Env
loadApp f = do
app <- Env.parse id $ App
<$> Env.switch "DRY_RUN" mempty
<*> LoggerEnv.parserNow you have application-specific actions that can do IO, log, and access your state:
myAppAction :: (MonadIO m, MonadLogger m, MonadReader App env) => m ()
myAppAction = do
isDryRun <- asks appDryRun
if isDryRun
then logWarn "Skipping due to dry-run"
else liftIO $ fireTheMisslesThese 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
runApp loadApp $ do
myAppAction
myOtherAppActionDatabase
Adding Database access requires an instance of on your HasSqlPoolApp
type. Most often, this will be easiest if you indeed separate a Config
attribute:
data Config = Config
{ configDbPoolSize :: Int
, configLogSettings :: LogSettings
}So you can isolate Env-related concerns
loadConfig :: IO Config loadConfig = Env.parse id $ Config <$> Env.var Env.auto "PGPOOLSIZE" (Env.def 1) <*> LoggerEnv.parser
from the runtime application state:
data App = App
{ appConfig :: Config
, appLogger :: Logger
, appSqlPool :: SqlPool
}
instance HasLogger App where
loggerL = appLogger $ \x y -> x { appLogger = y }
instance HasSqlPool App where
getSqlPool = appSqlPoolThe Freckle.App.Database module provides for
building a Pool given this (limited) config data:makePostgresPool
loadApp :: (App -> IO a) -> IO a
loadApp f = do
appConfig{..} <- loadConfig
appLogger <- newLogger configLoggerSettings
appSqlPool <- runLoggerLoggingT appLogger $ makePostgresPool configDbPoolSize
f App{..}This unlocks for your application:runDB
myAppAction :: (MonadIO m, MonadReader env m, HasSqlPool env) => SqlPersistT m [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 .aroundAll
Using MTL-style constraints (i.e. MonadReader) means you can use your
actions directly in expectations, but you may need some type annotations:
spec :: Spec
spec = aroundAll loadApp $ do
describe "myAppAction" $ do
it "works" $ do
result <- myAppAction :: AppExample App Text
result `shouldBe` "as expected"If your app type HasSqlPool, you can use runDB in your specs too:
spec :: Spec
spec = aroundAll loadApp $ do
describe "myQuery" $ do
it "works" $ do
result <- runDB myQuery :: AppExample App Text
result `shouldBe` "as expected"Documentation
runApp :: HasLogger app => (forall b. (app -> IO b) -> IO b) -> ReaderT app (LoggingT IO) a -> IO a Source #
module Freckle.App.Database
module Control.Monad.Reader
module Blammo.Logging