packages feed

box-0.6.3: app/websocket-tests.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -Wno-unused-do-bind #-}

{-

Models a websocket connection

-}

module Main where

import Box
import qualified Control.Concurrent.Classy.Async as C
import Control.Lens hiding (Unwrapped, Wrapped)
import Control.Monad.Catch
import Control.Monad.Conc.Class as C
import Data.Generics.Labels ()
import qualified Network.WebSockets as WS
import NumHask.Prelude hiding (STM, bracket)
import Options.Generic

data ConfigSocket = ConfigSocket
  { host :: !Text,
    port :: !Int,
    path :: !Text
  }
  deriving (Show, Eq, Generic)

defaultConfigSocket :: ConfigSocket
defaultConfigSocket = ConfigSocket "127.0.0.1" 9160 "/"

client :: (MonadIO m) => ConfigSocket -> WS.ClientApp () -> m ()
client c app = liftIO $ WS.runClient (unpack $ c ^. #host) (c ^. #port) (unpack $ c ^. #path) app

server :: (MonadIO m) => ConfigSocket -> WS.ServerApp -> m ()
server c app = liftIO $ WS.runServer (unpack $ c ^. #host) (c ^. #port) app

con :: (MonadMask m, MonadIO m) => WS.PendingConnection -> Cont m WS.Connection
con p = Cont $ \action ->
  bracket
    (liftIO $ WS.acceptRequest p)
    (\conn -> liftIO $ WS.sendClose conn ("Bye from con!" :: Text))
    action

clientApp ::
  (MonadIO m, MonadConc m) =>
  Box m (Either Text Text) Text ->
  WS.Connection ->
  m ()
clientApp (Box c e) conn =
  void $
    C.race
      (receiver c conn)
      (sender (Box mempty e) conn)

serverApp ::
  WS.PendingConnection ->
  IO ()
serverApp p =
  with
    (con p)
    ( responder
        (\x -> bool (Right $ "echo:" <> x) (Left "quit") (x == "q"))
        mempty
    )

serverIO :: IO ()
serverIO = server defaultConfigSocket serverApp

clientIO :: IO ()
clientIO =
  (client defaultConfigSocket . clientApp)
    (Box (contramap show toStdout) fromStdin)

data SocketType = Client | Responder | TestRun deriving (Eq, Read, Show, Generic)

instance ParseField SocketType

instance ParseRecord SocketType

instance ParseFields SocketType

newtype Opts w = Opts
  { apptype :: w ::: SocketType <?> "type of websocket app"
  }
  deriving (Generic)

instance ParseRecord (Opts Wrapped)

main :: IO ()
main = do
  o :: Opts Unwrapped <- unwrapRecord "example websocket apps"
  r :: Text <- case apptype o of
    Client -> show <$> clientIO
    Responder -> show <$> q serverIO
    TestRun -> show <$> testRun
  putStrLn r

-- | default websocket receiver
-- Lefts are info/debug
receiver ::
  (MonadIO m) =>
  Committer m (Either Text Text) ->
  WS.Connection ->
  m Bool
receiver c conn = go
  where
    go = do
      msg <- liftIO $ WS.receive conn
      case msg of
        WS.ControlMessage (WS.Close w b) ->
          commit
            c
            ( Left
                ( "receiver: received: close: " <> show w <> " " <> show b
                )
            )
        WS.ControlMessage _ -> go
        WS.DataMessage _ _ _ msg' -> do
          commit c $ Left $ "receiver: received: " <> (WS.fromDataMessage msg' :: Text)
          _ <- commit c (Right (WS.fromDataMessage msg'))
          go

-- | default websocket sender
sender ::
  (MonadIO m, WS.WebSocketsData a, Show a) =>
  Box m Text a ->
  WS.Connection ->
  m ()
sender (Box c e) conn = forever $ do
  msg <- emit e
  case msg of
    Nothing -> pure ()
    Just msg' -> do
      commit c $ "sender: sending: " <> (show msg' :: Text)
      liftIO $ WS.sendTextData conn msg'

-- | a receiver that responds based on received Text.
-- lefts are quit signals. Rights are response text.
responder ::
  (MonadIO m) =>
  (Text -> Either Text Text) ->
  Committer m Text ->
  WS.Connection ->
  m ()
responder f c conn = go
  where
    go = do
      msg <- liftIO $ WS.receive conn
      case msg of
        WS.ControlMessage (WS.Close _ _) -> do
          commit c "responder: normal close"
          liftIO $ WS.sendClose conn ("received close signal: responder closed." :: Text)
        WS.ControlMessage _ -> go
        WS.DataMessage _ _ _ msg' -> do
          case f $ WS.fromDataMessage msg' of
            Left _ -> do
              commit c "responder: sender initiated close"
              liftIO $ WS.sendClose conn ("received close signal: responder closed." :: Text)
            Right r -> do
              commit c ("responder: sending" <> r)
              liftIO $ WS.sendTextData conn r
              go

q :: IO a -> IO (Either () a)
q = race (cancelQ fromStdin)

cancelQ :: Emitter IO Text -> IO ()
cancelQ e = do
  e' <- emit e
  case e' of
    Just "q" -> pure ()
    _other -> do
      putStrLn ("nothing happens" :: Text)
      cancelQ e

-- | test of clientApp via a cRef committer and a canned list of Text
tClient :: [Text] -> IO [Either Text Text]
tClient xs = do
  (c, r) <- cRef
  client
    defaultConfigSocket
    ( \conn ->
        (`clientApp` conn)
          <$.> ( Box c <$> fromListE (xs <> ["q"])
               )
    )
  r

tClientIO :: [Text] -> IO ()
tClientIO xs =
  (client defaultConfigSocket . clientApp)
    <$.> (Box (contramap show toStdout) <$> fromListE (xs <> ["q"]))

-- | main test run of client-server functionality
-- the code starts a server in a thread, starts the client in the main thread, and cancels the server on completion.
-- > testRun
-- [Left "receiver: received: echo:1",Right "echo:1",Left "receiver: received: echo:2",Right "echo:2",Left "receiver: received: echo:3",Right "echo:3",Left "receiver: received: close: 1000 \"received close signal: responder closed.\""]
testRun :: IO [Either Text Text]
testRun = do
  a <- async (server defaultConfigSocket serverApp)
  r <- tClient (show <$> [1 .. 3 :: Int])
  cancel a
  pure r