hcheckers-0.1.0.0: src/Core/Rest.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Core.Rest where
import Control.Monad.Reader
import Control.Concurrent
import Control.Concurrent.STM
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Maybe
import Data.Aeson hiding ( json )
import Web.Scotty.Trans
import Network.HTTP.Types.Status
import System.Log.Heavy
import System.Log.Heavy.TH
import Core.Types
import Core.Board
import Core.Supervisor
import Core.Json ( ) -- import instances only
import Formats.Fen
import Formats.Pdn
type Rest a = ActionT Error Checkers a
error400 :: T.Text -> Rest ()
error400 message = do
json $ object ["error" .= message]
status status400
transformError :: Error -> Rest ()
transformError err = do
error400 $ T.pack $ show err
instance Parsable Side where
parseParam "1" = Right First
parseParam "2" = Right Second
parseParam text = Left $ "unknown side"
instance ScottyError Error where
stringError str = Unhandled str
showError err = TL.pack $ show err
withGameContext :: GameId -> Checkers a -> Checkers a
withGameContext gameId actions = withLogVariable "game" gameId actions
liftCheckers :: GameId -> Checkers a -> Rest a
liftCheckers gameId actions = liftCheckers' (Just gameId) actions
liftCheckers_ :: Checkers a -> Rest a
liftCheckers_ actions = liftCheckers' Nothing actions
liftCheckers' :: Maybe GameId -> Checkers a -> Rest a
liftCheckers' mbId actions = do
res <- lift $ wrap $ tryC actions
case res of
Right result -> return result
Left err -> raise err
where
wrap r = case mbId of
Nothing -> r
Just gameId -> withGameContext gameId r
boardRq :: SupervisorState -> SomeRules -> NewGameRq -> Rest (Maybe Side, Maybe BoardRep)
boardRq _ _ (NewGameRq { rqBoard = Just br, rqFen = Nothing, rqPdn = Nothing }) =
return $ (Nothing, Just br)
boardRq _ rules (NewGameRq { rqBoard = Nothing, rqFen = Just fen, rqPdn = Nothing })
= case parseFen rules fen of
Left err -> raise $ InvalidBoard err
Right (side, br) -> return (Just side, Just br)
boardRq rnd rules (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Just pdn })
= case parsePdn (Just rules) pdn of
Left err -> raise $ InvalidBoard err
Right gr -> return (Nothing, Just $ boardRep $ loadPdn rnd gr)
boardRq _ _ (NewGameRq { rqPrevBoard = Just gameId }) = do
board <- liftCheckers_ $ getInitialBoard gameId
return (Nothing, Just $ boardRep board)
boardRq _ _ (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Nothing }) =
return (Nothing, Nothing)
boardRq _ _ _ =
raise $ InvalidBoard "only one of fields must be filled: board, fen, pdn"
restServer :: MVar () -> ScottyT Error Checkers ()
restServer shutdownVar = do
defaultHandler transformError
post "/game/new" $ do
rq <- jsonData
case selectRules rq of
Nothing -> error400 "invalid game rules"
Just rules -> do
rnd <- liftCheckers_ $ do
sup <- askSupervisor
liftIO $ atomically $ readTVar sup
(mbFirstSide, board) <- boardRq rnd rules rq
let firstSide = fromMaybe First mbFirstSide
gameId <- liftCheckers_ $ newGame rules firstSide board
liftCheckers gameId $ $info
"Created new game #{}; First turn: {}; initial board: {}"
(gameId, show firstSide, show board)
json $ Response (NewGameRs gameId firstSide) []
post "/game/:id/attach/ai/:side" $ do
gameId <- param "id"
side <- param "side"
rules <- liftCheckers gameId $ getRules gameId
rq <- jsonData
case selectAi rq rules of
Nothing -> error400 "invalid ai settings"
Just ai -> do
liftCheckers gameId $ do
$info "Attached AI: {} to game #{} as {}" (show ai, gameId, show side)
initAiStorage rules ai
attachAi gameId side ai
json $ Response AttachAiRs []
post "/game/:id/attach/:name/:side" $ do
gameId <- param "id"
name <- param "name"
side <- param "side"
liftCheckers gameId $ do
registerUser gameId side name
$info "Attached player `{}' to game #{} as {}" (name, gameId, show side)
json $ Response RegisterUserRs []
post "/game/:id/run" $ do
gameId <- param "id"
liftCheckers gameId $ runGame gameId
json $ Response RunGameRs []
get "/game/:id/state" $ do
gameId <- param "id"
rs <- liftCheckers gameId $ getState gameId
json $ Response rs []
get "/game/:id/fen" $ do
gameId <- param "id"
rs <- liftCheckers gameId $ getFen gameId
Web.Scotty.Trans.text $ TL.fromStrict rs
get "/game/:id/pdn" $ do
gameId <- param "id"
rs <- liftCheckers gameId $ getPdn gameId
Web.Scotty.Trans.text $ TL.fromStrict rs
get "/game/:id/history" $ do
gameId <- param "id"
rs <- liftCheckers gameId $ getHistory gameId
json $ Response (HistoryRs rs) []
post "/game/:id/move/:name" $ do
gameId <- param "id"
name <- param "name"
moveRq <- jsonData
board <- liftCheckers gameId $ doMove gameId name moveRq
messages <- liftCheckers gameId $ getMessages name
json $ Response (MoveRs board) messages
get "/game/:id/moves/:name" $ do
gameId <- param "id"
name <- param "name"
side <- liftCheckers gameId $ getSideByUser gameId name
moves <- liftCheckers gameId $ getPossibleMoves gameId side
messages <- liftCheckers gameId $ getMessages name
json $ Response (PossibleMovesRs moves) messages
post "/game/:id/undo/:name" $ do
gameId <- param "id"
name <- param "name"
board <- liftCheckers gameId $ doUndo gameId name
messages <- liftCheckers gameId $ getMessages name
json $ Response (UndoRs board) messages
post "/game/:id/capitulate/:name" $ do
gameId <- param "id"
name <- param "name"
liftCheckers gameId $ doCapitulate gameId name
messages <- liftCheckers gameId $ getMessages name
json $ Response CapitulateRs messages
post "/game/:id/draw/request/:name" $ do
gameId <- param "id"
name <- param "name"
liftCheckers gameId $ doDrawRequest gameId name
messages <- liftCheckers gameId $ getMessages name
json $ Response DrawRqRs messages
post "/game/:id/draw/accept/:name" $ do
gameId <- param "id"
name <- param "name"
liftCheckers gameId $ doDrawAccept gameId name True
messages <- liftCheckers gameId $ getMessages name
json $ Response (DrawAcceptRs True) messages
post "/game/:id/draw/decline/:name" $ do
gameId <- param "id"
name <- param "name"
liftCheckers gameId $ doDrawAccept gameId name False
messages <- liftCheckers gameId $ getMessages name
json $ Response (DrawAcceptRs False) messages
get "/poll/:name" $ do
name <- param "name"
messages <- liftCheckers_ $ getMessages name
json $ Response (PollRs messages) []
get "/lobby/:rules" $ do
rules <- param "rules"
games <- liftCheckers_ $ getGames (Just rules)
json $ Response (LobbyRs games) []
get "/lobby" $ do
games <- liftCheckers_ $ getGames Nothing
json $ Response (LobbyRs games) []
get "/notation/:rules" $ do
rules <- param "rules"
(size, orientation, notation) <- liftCheckers_ $ getNotation rules
json $ Response (NotationRs size orientation notation) []
post "/server/shutdown" $ do
isLocal <- lift $ asks (gcLocal . csConfig)
if isLocal
then do
json $ Response ShutdownRs []
liftIO $ putMVar shutdownVar ()
else error400 "Server is not running in local mode"
runRestServer :: Checkers ()
runRestServer = do
cs <- ask
let getResponse m = do
res <- runCheckersT m cs
case res of
Right response -> return response
Left err -> fail $ show err
port <- asks (gcPort . csConfig)
shutdownVar <- liftIO newEmptyMVar
forkCheckers $ scottyT (fromIntegral port) getResponse (restServer shutdownVar)
liftIO $ takeMVar shutdownVar
-- REST thread should be able to write the response to Shutdown request.
liftIO $ threadDelay (1000 * 1000)