wrecker-1.0.0: examples/Server.hs
{-# LANGUAGE ScopedTypeVariables, TypeOperators, OverloadedStrings
#-}
{-# LANGUAGE DeriveGeneric, FlexibleInstances, QuasiQuotes #-}
{-# LANGUAGE CPP, FlexibleContexts, UndecidableInstances,
RecordWildCards #-}
{-# LANGUAGE DeriveFunctor, LambdaCase, OverloadedStrings #-}
{-# LANGUAGE TupleSections, GeneralizedNewtypeDeriving #-}
#ifndef _SERVER_IS_MAIN_
module Server where
#endif
import Control.Concurrent
import Control.Concurrent.NextRef (NextRef)
import qualified Control.Concurrent.NextRef as NextRef
import Control.Exception
import qualified Control.Immortal as Immortal
import Control.Monad.IO.Class
import Data.Aeson hiding (json)
import Data.Aeson.QQ
import Data.Maybe (listToMaybe)
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics
import Network.Socket (Socket)
import qualified Network.Socket as N
import qualified Network.Wai as Wai
import Network.Wai.Handler.Warp
(Port, defaultSettings, openFreePort)
import qualified Network.Wai.Handler.Warp as Warp
import System.Environment
import Web.Scotty (ActionM, ScottyM, json)
import qualified Web.Scotty as Scotty
import Wrecker
import Wrecker.Runner
import qualified Wrecker.Statistics as Wrecker
data Envelope a = Envelope
{ value :: a
} deriving (Show, Eq, Generic)
instance ToJSON a => ToJSON (Envelope a)
rootRef :: Int -> Text
rootRef port = T.pack $ "http://localhost:" ++ show port
jsonE :: ToJSON a => a -> ActionM ()
jsonE = json . Envelope
data Root a = Root
{ root :: a
, products :: a
, cartsIndex :: a
, cartsIndexItems :: a
, usersIndex :: a
, login :: a
, checkout :: a
} deriving (Show, Eq, Functor)
type RootInt = Root Int
instance Applicative Root where
pure x =
Root
{ root = x
, products = x
, login = x
, usersIndex = x
, cartsIndex = x
, cartsIndexItems = x
, checkout = x
}
f <*> x =
Root
{ root = root f $ root x
, products = products f $ products x
, login = login f $ login x
, usersIndex = usersIndex f $ usersIndex x
, cartsIndex = cartsIndex f $ cartsIndex x
, cartsIndexItems = cartsIndexItems f $ cartsIndexItems x
, checkout = checkout f $ checkout x
}
app :: RootInt -> Port -> ScottyM ()
app Root {..} port = do
let host = rootRef port
Scotty.get "/root" $ do
liftIO $ threadDelay root
jsonE
[aesonQQ|
{ "products" : #{host <> "/products" }
, "carts" : #{host <> "/carts" }
, "users" : #{host <> "/users" }
, "login" : #{host <> "/login" }
, "checkout" : #{host <> "/checkout" }
}
|]
Scotty.get "/products" $ do
liftIO $ threadDelay products
jsonE
[aesonQQ|
[ #{host <> "/products/0"}
]
|]
Scotty.get "/product/:id" $ do
liftIO $ threadDelay products
jsonE
[aesonQQ|
{ "summary" : "shirt" }
|]
Scotty.get "/carts" $
-- sleepDist gen carts
do
jsonE
[aesonQQ|
[ #{host <> "/carts/0"}
]
|]
Scotty.get "/carts/:id" $ do
liftIO $ threadDelay cartsIndex
jsonE
[aesonQQ|
{ "items" : #{host <> "/carts/0/items"}
}
|]
Scotty.post "/carts/:id/items" $ do
liftIO $ threadDelay cartsIndexItems
jsonE
[aesonQQ|
#{host <> "/carts/0/items"}
|]
Scotty.get "/users" $
-- sleepDist gen users
do
jsonE
[aesonQQ|
[ #{host <> "/users/0"}
]
|]
Scotty.get "/users/:id" $ do
liftIO $ threadDelay usersIndex
jsonE
[aesonQQ|
{ "cart" : #{host <> "/carts/0"}
, "username" : "example"
}
|]
Scotty.post "/login" $ do
liftIO $ threadDelay login
jsonE
[aesonQQ|
#{host <> "/users/0"}
|]
Scotty.post "/checkout" $ do
liftIO $ threadDelay checkout
jsonE ()
run :: RootInt -> IO (Port, Immortal.Thread, ThreadId, NextRef AllStats)
run = start Nothing
stop :: (Port, ThreadId, NextRef AllStats) -> IO AllStats
stop (_, threadId, ref) = do
killThread threadId
NextRef.readLast ref
toKey :: Wai.Request -> String
toKey x =
case Wai.pathInfo x of
["root"] -> "/root"
["products"] -> "/products"
"carts":_:"items":_ -> "/carts/0/items"
"carts":_:_ -> "/carts/0"
"users":_ -> "/users/0"
["login"] -> "/login"
["checkout"] -> "/checkout"
_ -> error "FAIL! UNKNOWN REQUEST FOR EXAMPLE!"
recordMiddleware :: Recorder -> Wai.Application -> Wai.Application
recordMiddleware recorder waiApp req sendResponse =
record recorder (toKey req) $! waiApp req $ \res -> sendResponse res
getASocket :: Maybe Port -> IO (Port, Socket)
getASocket =
\case
Just port -> do
s <- N.socket N.AF_INET N.Stream N.defaultProtocol
localhost <- N.inet_addr "127.0.0.1"
N.bind s (N.SockAddrInet (fromIntegral port) localhost)
N.listen s 1000
return (port, s)
Nothing -> openFreePort
start :: Maybe Port -> RootInt -> IO (Port, Immortal.Thread, ThreadId, NextRef AllStats)
start mport dist = do
(port, socket) <- getASocket mport
(ref, recorderThread, recorder) <- newStandaloneRecorder
scottyApp <- Scotty.scottyApp $ app dist port
threadId <-
flip forkFinally (\_ -> N.close socket) $
Warp.runSettingsSocket defaultSettings socket $ recordMiddleware recorder $ scottyApp
return (port, recorderThread, threadId, ref)
main :: IO ()
main = do
xs <- getArgs
let delay = maybe 0 read $ listToMaybe xs
(port, socket) <- getASocket $ Just 3000
(ref, recorderThread, recorder) <- newStandaloneRecorder
scottyApp <- Scotty.scottyApp $ app (pure delay) port
(Warp.runSettingsSocket defaultSettings socket $ recordMiddleware recorder $ scottyApp) `finally`
(do N.close socket
Immortal.stop recorderThread
allStats <- NextRef.readLast ref
putStrLn $ Wrecker.pprStats Nothing Path allStats)