-- | 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 @'IORef'@ if you need mutable application state.
--
-- 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 @'HasSqlPool'@ on your @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 @'makePostgresPool'@ for
-- building a Pool given this (limited) config data:
--
-- > 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 @'runDB'@ for your application:
--
-- > myAppAction :: AppM [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 @'before'@.
--
-- Our @Test@ module also exposes @'runAppTest'@ for running @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 @'runDB'@ in tests to avoid excessive
-- annotations because of the generalized type of @'runDB'@ itself:
--
-- > 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"
--
module Freckle.App
  ( runApp
  , module X
  )
where

import Prelude

import Control.Monad.Logger as X
import Control.Monad.Reader as X
import Freckle.App.Database as X
import Freckle.App.Logging as X
import System.IO (BufferMode(..), hSetBuffering, stderr, stdout)

runApp :: HasLogging app => app -> ReaderT app (LoggingT IO) a -> IO a
runApp :: app -> ReaderT app (LoggingT IO) a -> IO a
runApp app
app ReaderT app (LoggingT IO) a
action = do
  -- Ensure output is streamed if in a Docker container
  Handle -> BufferMode -> IO ()
hSetBuffering Handle
stdout BufferMode
LineBuffering
  Handle -> BufferMode -> IO ()
hSetBuffering Handle
stderr BufferMode
LineBuffering
  app -> LoggingT IO a -> IO a
forall a b. HasLogging a => a -> LoggingT IO b -> IO b
runAppLoggerT app
app (LoggingT IO a -> IO a) -> LoggingT IO a -> IO a
forall a b. (a -> b) -> a -> b
$ ReaderT app (LoggingT IO) a -> app -> LoggingT IO a
forall r (m :: * -> *) a. ReaderT r m a -> r -> m a
runReaderT ReaderT app (LoggingT IO) a
action app
app