> {-# LANGUAGE FlexibleContexts #-}
> {-# LANGUAGE GeneralizedNewtypeDeriving #-}
> {-# LANGUAGE OverloadedStrings #-}
> module Hairy where
> import Control.Applicative (Applicative)
> import Control.Monad.IO.Class (MonadIO, liftIO)
> import Control.Monad.Logger (runNoLoggingT, runStdoutLoggingT)
> import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT)
> import Control.Monad.Trans.Class (MonadTrans, lift)
> import Data.Aeson (Value (Null), (.=), object)
> import Data.Default (def)
> import Data.Text.Lazy (Text)
> import qualified Database.Persist as DB
> import qualified Database.Persist.Postgresql as DB
> import Hairy.Models (Task, TaskId, migrateAll)
> import Network.HTTP.Types.Status (created201, internalServerError500, notFound404)
> import Network.Wai (Middleware)
> import Network.Wai.Handler.Warp (defaultSettings)
> import Network.Wai.Middleware.RequestLogger (logStdout, logStdoutDev)
> import System.Environment (lookupEnv)
> import Web.Scotty.Trans (ActionT, Options, ScottyT, defaultHandler, delete,
>   get, json, jsonData, middleware, notFound, param, post, put, scottyOptsT,
>   settings, showError, status, verbose)
> main :: IO ()
> main = do
>   c <- getConfig
>   runApplication c
> getConfig :: IO Config
> getConfig = do
>   e <- getEnvironment
>   p <- getPool e
>   return Config
>     { environment = e
>     , pool = p
>     }
> data Config = Config
>   { environment :: Environment
>   , pool :: DB.ConnectionPool
>   }
> getEnvironment :: IO Environment
> getEnvironment = do
>   m <- lookupEnv "SCOTTY_ENV"
>   let e = case m of
>         Nothing -> Development
>         Just s -> read s
>   return e
> getPool :: Environment -> IO DB.ConnectionPool
> getPool e =
>   case e of
>     Development -> runStdoutLoggingT (DB.createPostgresqlPool s n)
>     Production -> runStdoutLoggingT (DB.createPostgresqlPool s n)
>     Test -> runNoLoggingT (DB.createPostgresqlPool s n)
>   where
>     s = getConnectionString e
>     n = getConnectionSize e
> getConnectionString :: Environment -> DB.ConnectionString
> getConnectionString Development =
>   "host=localhost port=5432 user=taylor dbname=hairy_development"
> getConnectionString Production =
>   "host=localhost port=5432 user=taylor dbname=hairy_production"
> getConnectionString Test =
>   "host=localhost port=5432 user=taylor dbname=hairy_test"
> getConnectionSize :: Environment -> Int
> getConnectionSize Development = 1
> getConnectionSize Production = 8
> getConnectionSize Test = 1
> data Environment
>   = Development
>   | Production
>   | Test
>   deriving (Eq, Read, Show)
> runApplication :: Config -> IO ()
> runApplication c = do
>   let o = getOptions (environment c)
>       r m = runReaderT (runConfigM m) c
>   scottyOptsT o r r application
> newtype ConfigM a = ConfigM
>  { runConfigM :: ReaderT Config IO a
>  } deriving (Applicative, Functor, Monad, MonadIO, MonadReader Config)
> getOptions :: Environment -> Options
> getOptions Development = def
> getOptions Production = def
>   { settings = defaultSettings
>   , verbose = 0
>   }
> getOptions Test = def
>   { verbose = 0
>   }
> type Error = Text
> application :: ScottyT Error ConfigM ()
> application = do
>   runDB (DB.runMigration migrateAll)
>   e <- lift (asks environment)
>   middleware (loggingM e)
>   defaultHandler (defaultH e)
>   get "/tasks" getTasksA
>   post "/tasks" postTasksA
>   get "/tasks/:id" getTaskA
>   put "/tasks/:id" putTaskA
>   delete "/tasks/:id" deleteTaskA
>   notFound notFoundA
> runDB :: (MonadTrans t, MonadIO (t ConfigM)) => DB.SqlPersistT IO a -> t ConfigM a
> runDB q = do
>   p <- lift (asks pool)
>   liftIO (DB.runSqlPool q p)
> loggingM :: Environment -> Middleware
> loggingM Development = logStdoutDev
> loggingM Production = logStdout
> loggingM Test = id
> type Action = ActionT Error ConfigM ()
> defaultH :: Environment -> Error -> Action
> defaultH e x = do
>   status internalServerError500
>   case e of
>     Development -> json (object ["error" .= showError x])
>     Production -> json Null
>     Test -> json (object ["error" .= showError x])
> getTasksA :: Action
> getTasksA = do
>   ts <- runDB (DB.selectList [] [])
>   json (ts :: [DB.Entity Task])
> postTasksA :: Action
> postTasksA = do
>   t <- jsonData
>   runDB (DB.insert_ t)
>   status created201
>   json (t :: Task)
> getTaskA :: Action
> getTaskA = do
>   i <- param "id"
>   m <- runDB (DB.get (toKey i))
>   case m of
>     Nothing -> notFoundA
>     Just t -> json (t :: Task)
> putTaskA :: Action
> putTaskA = do
>   i <- param "id"
>   t <- jsonData
>   runDB (DB.repsert (toKey i) t)
>   json (t :: Task)
> deleteTaskA :: Action
> deleteTaskA = do
>   i <- param "id"
>   runDB (DB.delete (toKey i :: TaskId))
>   json Null
> toKey :: DB.ToBackendKey DB.SqlBackend a => Integer -> DB.Key a
> toKey i = DB.toSqlKey (fromIntegral (i :: Integer))
> notFoundA :: Action
> notFoundA = do
>   status notFound404
>   json Null