freckle-app-1.8.0.0: Haskell application toolkit used at Freckle
Safe HaskellSafe-Inferred
LanguageHaskell2010

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 IORef if you need mutable application state.

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.parser

Now 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 $ 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
  runApp loadApp $ do
    myAppAction
    myOtherAppAction

Database

Adding Database access requires an instance of HasSqlPool on your App 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 = appSqlPool

The Freckle.App.Database module provides makePostgresPool for building a Pool given this (limited) config data:

loadApp :: (App -> IO a) -> IO a
loadApp f = do
  appConfig{..} <- loadConfig
  appLogger <- newLogger configLoggerSettings
  appSqlPool <- runLoggerLoggingT appLogger $ makePostgresPool configDbPoolSize
  f App{..}

This unlocks runDB for your application:

myAppAction
  :: (MonadIO m, MonadReader env m, HasSqlPool env)
  => SqlPersistT m [Entity Something]
myAppAction = runDB $ selectList [] []

Testing

Freckle.App.Test exposes an AppExample type for examples in a SpecWith App spec. The can be run by giving your loadApp 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 #