|
@@ -0,0 +1,172 @@
|
|
|
+{-# LANGUAGE OverloadedStrings #-}
|
|
|
+
|
|
|
+module Lib (
|
|
|
+ main
|
|
|
+ , Db.Config(..)
|
|
|
+) where
|
|
|
+
|
|
|
+import qualified Lib.Types as Types
|
|
|
+import qualified Lib.Db as Db
|
|
|
+import qualified Data.Either as Either
|
|
|
+import qualified Data.Maybe as Maybe
|
|
|
+import Data.List (sortOn)
|
|
|
+import Control.Monad (replicateM, join)
|
|
|
+
|
|
|
+import qualified Data.Pool as Pool
|
|
|
+import qualified Data.ByteString.Lazy as LBS
|
|
|
+import qualified Data.ByteString.Lazy.Char8 as LBSC
|
|
|
+import qualified Network.HTTP.Types.Status as Status
|
|
|
+import qualified Network.HTTP.Types.Header as Header
|
|
|
+import qualified Network.Wai as Wai
|
|
|
+import qualified Network.Wai.Handler.Warp as Warp
|
|
|
+import qualified Data.BufferBuilder.Json as Json
|
|
|
+import Data.BufferBuilder.Json ((.=))
|
|
|
+import qualified System.Random.MWC as MWC
|
|
|
+import qualified Data.Vector as V
|
|
|
+import qualified Html
|
|
|
+import Html ((#))
|
|
|
+
|
|
|
+-- entry point
|
|
|
+main :: Db.Config -> IO ()
|
|
|
+main dbConfig = do
|
|
|
+ putStrLn "Config is:"
|
|
|
+ print dbConfig
|
|
|
+ putStrLn "Initializing database connection pool..."
|
|
|
+ dbPool <- Db.mkPool dbConfig
|
|
|
+ putStrLn "Initializing PRNG seed..."
|
|
|
+ gen <- MWC.create
|
|
|
+ putStrLn "Warp core online: using postgres-wire"
|
|
|
+ Warp.run 7041 $ app gen dbPool
|
|
|
+
|
|
|
+-- router
|
|
|
+app :: MWC.GenIO -> Db.Pool -> Wai.Application
|
|
|
+app gen dbPool req respond = do
|
|
|
+ let qParams = Wai.queryString req
|
|
|
+ let mCount = Types.parseCount =<< join (lookup "queries" qParams)
|
|
|
+ case (Wai.requestMethod req, Wai.pathInfo req) of
|
|
|
+ ("GET", ["plaintext"])
|
|
|
+ -> respond getPlaintext
|
|
|
+ ("GET", ["json"])
|
|
|
+ -> respond getJson
|
|
|
+ ("GET", ["db"])
|
|
|
+ -> respond =<< getWorld gen dbPool
|
|
|
+ ("GET", ["fortune"])
|
|
|
+ -> respond =<< getFortunes dbPool
|
|
|
+ ("GET", ["queries"])
|
|
|
+ -> respond =<< getWorlds gen dbPool mCount
|
|
|
+ ("GET", ["updates"])
|
|
|
+ -> respond =<< updateWorlds gen dbPool mCount
|
|
|
+ _ -> respond routeNotFound
|
|
|
+
|
|
|
+-- * response helpers
|
|
|
+
|
|
|
+contentText :: Header.ResponseHeaders
|
|
|
+contentText = [(Header.hContentType, "text/plain")]
|
|
|
+
|
|
|
+respondText :: Status.Status -> LBS.ByteString -> Wai.Response
|
|
|
+respondText code = Wai.responseLBS code contentText
|
|
|
+
|
|
|
+contentJson :: Header.ResponseHeaders
|
|
|
+contentJson = [(Header.hContentType, "application/json")]
|
|
|
+
|
|
|
+{-# SPECIALIZE respondJson :: Json.ObjectBuilder -> Wai.Response #-}
|
|
|
+{-# SPECIALIZE respondJson :: Types.World -> Wai.Response #-}
|
|
|
+respondJson :: Json.ToJson a => a -> Wai.Response
|
|
|
+respondJson = Wai.responseLBS Status.status200 contentJson . mkBs
|
|
|
+ where
|
|
|
+ mkBs = LBS.fromStrict . Json.encodeJson
|
|
|
+
|
|
|
+contentHtml :: Header.ResponseHeaders
|
|
|
+contentHtml = [(Header.hContentType, "text/html; charset=UTF-8")]
|
|
|
+
|
|
|
+respondHtml :: Types.FortunesHtml -> Wai.Response
|
|
|
+respondHtml = Wai.responseLBS Status.status200 contentHtml . Html.renderByteString
|
|
|
+
|
|
|
+-- * error responses
|
|
|
+
|
|
|
+routeNotFound :: Wai.Response
|
|
|
+routeNotFound = respondText Status.status400 "Bad route"
|
|
|
+
|
|
|
+entityNotFound :: Wai.Response
|
|
|
+entityNotFound = respondText Status.status404 "Not Found"
|
|
|
+
|
|
|
+respondInternalError :: LBS.ByteString -> Wai.Response
|
|
|
+respondInternalError = respondText Status.status500
|
|
|
+
|
|
|
+respondDbError :: Db.Error -> Wai.Response
|
|
|
+respondDbError = respondInternalError . LBSC.pack . show
|
|
|
+
|
|
|
+respondDbErrors :: [Db.Error] -> Wai.Response
|
|
|
+respondDbErrors = respondInternalError . LBSC.pack . show
|
|
|
+
|
|
|
+-- * route implementations
|
|
|
+
|
|
|
+getPlaintext :: Wai.Response
|
|
|
+getPlaintext = respondText Status.status200 "Hello, World!"
|
|
|
+{-# INLINE getPlaintext #-}
|
|
|
+
|
|
|
+getJson :: Wai.Response
|
|
|
+getJson = respondJson $ "message" .= Types.unsafeJsonString "Hello, World!"
|
|
|
+{-# INLINE getJson #-}
|
|
|
+
|
|
|
+getWorld :: MWC.GenIO -> Db.Pool -> IO Wai.Response
|
|
|
+getWorld gen dbPool = do
|
|
|
+ wId <- randomId gen
|
|
|
+ Pool.withResource dbPool $ \conn -> do
|
|
|
+ res <- Db.queryWorldById conn wId
|
|
|
+ pure . mkResponse $ res
|
|
|
+ where
|
|
|
+ mkSuccess = Maybe.maybe entityNotFound respondJson . flip (V.!?) 0
|
|
|
+ mkResponse = Either.either respondDbError mkSuccess
|
|
|
+{-# INLINE getWorld #-}
|
|
|
+
|
|
|
+getWorlds :: MWC.GenIO -> Db.Pool -> Maybe Types.Count -> IO Wai.Response
|
|
|
+getWorlds gen dbPool mCount = do
|
|
|
+ wIds <- replicateM count $ randomId gen
|
|
|
+ Pool.withResource dbPool $ \conn -> do
|
|
|
+ res <- Db.queryWorldByIds conn wIds
|
|
|
+ pure . mkResponse $ res
|
|
|
+ where
|
|
|
+ count = Types.getCount mCount
|
|
|
+ mkResponse = Either.either respondDbErrors respondJson
|
|
|
+{-# INLINE getWorlds #-}
|
|
|
+
|
|
|
+updateWorlds :: MWC.GenIO -> Db.Pool -> Maybe Types.Count -> IO Wai.Response
|
|
|
+updateWorlds gen dbPool mCount = do
|
|
|
+ wIds <- replicateM count $ randomId gen
|
|
|
+ Pool.withResource dbPool $ \conn -> do
|
|
|
+ res <- Db.queryWorldByIds conn wIds
|
|
|
+ Either.either (pure . respondDbErrors) (go conn) res
|
|
|
+ where
|
|
|
+ count = Types.getCount mCount
|
|
|
+ mkResponse = Either.either respondDbErrors respondJson
|
|
|
+ go conn ws = do
|
|
|
+ wNumbers <- replicateM count $ randomId gen
|
|
|
+ wsUp <- Db.updateWorlds conn . zip ws $ fmap fromIntegral wNumbers
|
|
|
+ return $ mkResponse wsUp
|
|
|
+{-# INLINE updateWorlds #-}
|
|
|
+
|
|
|
+getFortunes :: Db.Pool -> IO Wai.Response
|
|
|
+getFortunes dbPool = do
|
|
|
+ Pool.withResource dbPool $ \conn -> do
|
|
|
+ res <- Db.queryFortunes conn
|
|
|
+ return $ case res of
|
|
|
+ Left e -> respondDbError e
|
|
|
+ Right fs -> respondHtml $ do
|
|
|
+ let new = Types.Fortune 0 "Additional fortune added at request time."
|
|
|
+ let header = Html.tr_ $ Html.th_ (Html.Raw "id") # Html.th_ (Html.Raw "message")
|
|
|
+ let mkRow f = Html.tr_ $ Html.td_ (fromIntegral $ Types.fId f) # Html.td_ (Types.fMessage $ f)
|
|
|
+ let rows = fmap mkRow $ sortOn Types.fMessage (new : V.toList fs)
|
|
|
+ Html.doctype_ #
|
|
|
+ Html.html_ (
|
|
|
+ Html.head_ (
|
|
|
+ Html.title_ (Html.Raw "Fortunes")
|
|
|
+ ) #
|
|
|
+ Html.body_ ( Html.table_ $
|
|
|
+ header # rows
|
|
|
+ )
|
|
|
+ )
|
|
|
+{-# INLINE getFortunes #-}
|
|
|
+
|
|
|
+randomId :: MWC.GenIO -> IO Types.QId
|
|
|
+randomId = MWC.uniformR (1, 10000)
|