aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuebinm2021-03-07 16:49:26 +0100
committerstuebinm2021-03-07 16:49:26 +0100
commit936dc3d58c9e7f388a2df92c182abeaeca52ce17 (patch)
tree00f1d9e2e89611512462c21926e529b05f26ee01
parent9bea2e88fa7db10756353abcb4c7230af309085a (diff)
Better handling of json
(mostly to reduce the number of record types needed, and to deal with multiple possible types of client messages)
Diffstat (limited to '')
-rw-r--r--Main.lhs67
1 files changed, 47 insertions, 20 deletions
diff --git a/Main.lhs b/Main.lhs
index c05c2ab..3586795 100644
--- a/Main.lhs
+++ b/Main.lhs
@@ -7,13 +7,14 @@ example of the haskell websockets library.
> {-# LANGUAGE OverloadedStrings #-}
> {-# LANGUAGE DeriveGeneric #-}
+> {-# LANGUAGE DeriveAnyClass #-}
> module Main where
> import Data.Aeson
> import GHC.Generics
> import Data.Text (Text)
> import Data.HashMap.Strict (HashMap)
> import Control.Exception (finally)
-> import Control.Monad (forM_, forever)
+> import Control.Monad (forM_, forever, mplus, mzero)
> import Control.Concurrent (MVar, newMVar, modifyMVar_, modifyMVar, readMVar, takeMVar, putMVar)
> import qualified Data.Text as T
> import qualified Data.Text.Encoding as T
@@ -51,22 +52,44 @@ an empty map:
> initialState = M.empty
-
Protocol
========
-Clients communicate by sending strings containing json over websockets,
-though there are currently only two possible messages, both of which
-are pretty boring:
+Clients communicate by sending strings containing json over websockets.
+
+Upon establishing a connection, clients declare which room they wish
+to join:
+
+> data Join = Join { room :: !Text }
+> deriving (Show, Generic, FromJSON)
+
+After joining, clients can either change the room state, or point at
+some particular position on that room's canvas:
+
+> data ClientMsg = ClientState Int | ClientPointAt Point
+> deriving (Show)
-> data Join = Join { room :: !Text } deriving (Show, Generic)
-> data State = State { state :: Int } deriving (Show, Generic)
-> instance FromJSON Join
-> instance FromJSON State
-> instance ToJSON State
+> data Point = Point { x :: Int, y :: Int }
+> deriving (Show, Generic, FromJSON, ToJSON)
-Join is sent after a connection is established to indicate which room
-should be joined, State to indicate a state change
+Parsing these incoming messages correctly requires a bit of boilerplate
+(or at least I've not yet found out how to get Aeson and Generics or
+TemplateHaskell to do this automatically):
+
+> instance FromJSON ClientMsg where
+> parseJSON (Object v) = parseState `mplus` parsePointAt
+> where
+> parseState = ClientState <$> v .: "state"
+> parsePointAt = ClientPointAt <$> v .: "pointat"
+> parseJSON _ = mzero
+
+The protocol for incoming messages is therefore that '{"state":2}' should
+set the room state to 2, and that '{"pointat":{"x":10,"y":20}}' points
+at position (10,20).
+
+The server replies (and broadcasts to other clients) in similar json
+messages which are constructed ad-hoc and don't have their own types
+(for now).
@@ -125,7 +148,7 @@ to the usual message handling loop, which just needs the room's state,
not the server's global state:
> putStrLn $ show i <> " joined room " <> (show $ room join)
-> WS.sendTextData conn $ encode State { state = n }
+> WS.sendTextData conn $ encode (object ["state" .= n])
> talk (i, conn) roomstate
Only one thing is still left to do, which is to define the `insertClient`
@@ -145,8 +168,6 @@ of `modifyMVar` above look nicer.
Message Loop
============
-Still todo: how to dynamically handle different incoming data types via Aeson?
-
Before we start the message loop, we first set up a disconnect handler
which will remove the client from the room's state once the socket closes.
@@ -159,12 +180,18 @@ it did drop them).
> talk :: Client -> MVar Room -> IO ()
> talk (i,conn) roomstate = flip finally (disconnect i) $ forever $ do
> msg <- WS.receiveData conn
-> let d = (eitherDecode msg) :: (Either String State)
+> let d = (eitherDecode msg) :: (Either String ClientMsg)
> case d of
-> Left err -> putStrLn $ "json malformed" <> err
-> Right new -> do
-> peers <- modifyMVar roomstate $ \(cs,n) -> return ((cs, state new), cs)
-> broadcast (encode $ State { state = state new }) peers
+> Left err -> putStrLn $ "json malformed: " <> err
+> Right msg -> case msg of
+> ClientState new -> do
+> clients <- modifyMVar roomstate $ \(cs,n) -> return ((cs, new), cs)
+> broadcast (encode $ object ["state" .= new]) clients
+> ClientPointAt point -> do
+> (clients,_) <- readMVar roomstate
+> let peers = filter (\(i',_) -> i' /= i) clients
+> putStrLn $ "client points at " <> show point
+> broadcast (encode $ object ["point" .= point, "id" .= i]) peers
> where
> disconnect i = do
> modifyMVar_ roomstate (\room -> return $ removeClient i room)