Skip to content

Commit

Permalink
Flatten the main function
Browse files Browse the repository at this point in the history
This should make it a bit easier to read while hopefully making it
easier to change in the future.

Use pattern matching to see if the user wants to the see the usage
instructions, this allows us to make use of one `case` expression
instead of a `case` expression and an `if` expression.

Do configuration reification pointfree style while using lenses.

Co-authored-by: Jacob "Jassob" Jonsson <[email protected]>
  • Loading branch information
Rembane and Jassob committed Sep 27, 2023
1 parent ddb9040 commit ca718be
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 49 deletions.
95 changes: 47 additions & 48 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

module Main
( main
)
where
) where

import Control.Concurrent ( MVar
, newMVar
, threadDelay
, tryPutMVar
)
import qualified Control.Concurrent.Async as Async
import qualified Control.Concurrent.Async as Async
import Control.Monad ( forever )
import Control.Monad.Log ( defaultBatchingOptions
, renderWithTimestamp
Expand All @@ -20,15 +19,15 @@ import Control.Monad.Log ( defaultBatchingOptio
import Control.Monad.Reader ( runReaderT )
import Control.Monad.Trans ( liftIO )
import Data.FileEmbed ( embedDir )
import Data.Foldable ( traverse_ )
import Data.IORef ( IORef
, readIORef
)
import Data.Time.Format ( defaultTimeLocale
, formatTime
, iso8601DateFormat
)
import Lens.Micro.Platform ( (<&>)
, set
import Lens.Micro.Platform ( set
, view
)
import Network.HTTP.Client.TLS ( newTlsManager )
Expand Down Expand Up @@ -56,63 +55,63 @@ import View ( render )

opts :: [OptDescr (Config -> Config)]
opts =
[ Option [] ["help"] (NoArg (set cHelp True)) "Show usage info"
, Option [] ["port"] (ReqArg (set cPort . read) "PORT") "Port to run on"
[ Option [] ["help"] (NoArg (set cHelp True)) "Show usage info"
, Option [] ["port"] (ReqArg (set cPort . read) "PORT") "Port to run on"
, Option []
["interval"]
(ReqArg (set cInterval . (1000000 *) . read) "INTERVAL (s)")
"Update interval"
]

usage :: IO ()
usage = putStrLn $ usageInfo "mat-chalmers [OPTION...]" opts

main :: IO ()
main =
getArgs
<&> getOpt Permute opts
>>= \case
(_ , _ , _ : _) -> usage
(_ , _ : _, _ ) -> usage
(!confs, _ , _ ) -> do
let config = foldl (flip id) defaultConfig confs
if view cHelp config
then usage
else do
upd <- newMVar () -- putMVar when to update
mgr <- newTlsManager
(viewRef, refreshAction) <- runLoggingT
(runReaderT refresh (ClientContext config mgr))
print
main = (reifyConfig . getOpt Permute opts <$> getArgs) >>= \case
(_ , _ , _ : _) -> usage
(_ , _ : _, _ ) -> usage
(Config { _cHelp = True }, _ , _ ) -> usage
(config , _ , _ ) -> do
upd <- newMVar () -- putMVar when to update
mgr <- newTlsManager
(viewRef, refreshAction) <- runLoggingT
(runReaderT refresh (ClientContext config mgr))
print

-- In the list there are three items running concurrently:
-- 1. Timer that sends a signal to the updater when it's time to update
-- 2. Webserver that serves the menus to the user
-- 3. Updater that fetches new data from the restaurants
Async.runConcurrently $ traverse_
Async.Concurrently
[ timer upd config
, webserver config viewRef upd
, updater mgr upd refreshAction config
]
where
timer upd cfg =
forever $ tryPutMVar upd () >> threadDelay (view cInterval cfg)

Async.concurrently_
(Async.concurrently_
-- timer
(forever $ tryPutMVar upd () >> threadDelay (view cInterval config))
-- webserver
(serve config viewRef upd))
-- updater
(forever
$ withFDHandler defaultBatchingOptions stdout 1.0 80
$ \logCallback ->
runReaderT (refreshAction upd) (ClientContext config mgr)
`runLoggingT` ( logCallback
. renderWithTimestamp
(formatTime
defaultTimeLocale
(iso8601DateFormat
(Just "%H:%M:%S")
)
)
id
))
where usage = putStrLn $ usageInfo "mat-chalmers [OPTION...]" opts
updater mgr upd refreshAction cfg =
forever
$ withFDHandler defaultBatchingOptions stdout 1.0 80
$ \logCallback -> runLoggingT
(runReaderT (refreshAction upd) (ClientContext cfg mgr))
( logCallback
. renderWithTimestamp
(formatTime defaultTimeLocale
(iso8601DateFormat (Just "%H:%M:%S"))
)
id
)

serve
webserver
:: Config
-> IORef View -- ^ View model
-> MVar () -- ^ Update signal
-> IO ()
serve conf viewRef upd = scotty (view cPort conf) $ do
webserver conf viewRef upd = scotty (view cPort conf) $ do
middleware logStdout
middleware (static $(embedDir "static"))
get "/" ((html . render) =<< liftIO (readIORef viewRef))
get "/r" (liftIO (tryPutMVar upd ()) >> redirect "/") -- force update

16 changes: 15 additions & 1 deletion src/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

module Config where

import Data.Foldable ( foldl' )
import Lens.Micro.Platform

-- | Configuration record
Expand All @@ -12,9 +13,22 @@ data Config = Config
, _cNextDayHour :: !Int
, _cInterval :: !Int
, _cPort :: !Int
} deriving (Eq, Show)
}
deriving (Eq, Show)

makeLenses ''Config

defaultConfig :: Config
defaultConfig = Config False 14 (1000000 * 60 * 30) 5007

-- | Create a Config we can touch
--
-- Yes, this function implementation is overly cute. It uses a lens to update
-- the first field of the tuple by using defaultConfig as first argument to
-- every function in the list. The brain needs to turn inside out a couple of
-- times before it makes sense, so if you're in a hurry, look at the types.
--
-- TODO: Feel free to bikeshed the function name.
reifyConfig
:: ([Config -> Config], [String], [String]) -> (Config, [String], [String])
reifyConfig = (& _1 %~ foldl' (flip id) defaultConfig)

0 comments on commit ca718be

Please sign in to comment.