packages feed

network-rpca-0.0.1: src/Network/RPCA/ExportedService.hs

-- | An ExportedService is a set of methods which can be involved over the
--   network. Each method has a name, which is unique within the service and
--   the name of the service is unique within the Port that it's bound to.
module Network.RPCA.ExportedService
  ( Callback
  , DispatchTable
  , MethodInfo(..)
  , State(..)
  , newService
  , rpcFunction
  ) where

import Control.Concurrent.STM

import qualified Data.ByteString as BS
import qualified Data.Map as Map

import Network.RPCA.Structs
import Network.RPCA.Util
import qualified Network.RPCA.Port as Port

import qualified Codec.Libevent.Class as Tagged

type DispatchTable = Map.Map String MethodInfo

-- | This is the type of a callback from the RPC system to the client
--   application. The first ByteString is the payload of the RPC
--   and the application should call the callback with a reply structure
--   that has the reply_code filled in
type Callback = Rpcrequest
              -> BS.ByteString
              -> (Rpcreply -> BS.ByteString -> STM ())
              -> IO ()

data MethodInfo = MethodInfo { methcallback :: Callback
                             , methargument :: ()  -- ^ for future reflection
                             , methreply :: () -- ^ for future reflection
                             }

data Service = Service { servname :: String
                       , servstate :: TVar State
                       , servport :: Port.Port
                       , servcbs :: DispatchTable }

data State = Up | Lame | Down deriving (Show, Eq)

-- | This wraps a callback function into the format expected by newService.
--   Until this point, the RPC system is payload format agnostic. This
--   wrapper introduces the assumption that the payload is a codec-libevent
--   structure.
rpcFunction :: (Tagged.TaggedStructure a, Tagged.TaggedStructure b)
            => (a -> ((Maybe b) -> STM ()) -> IO ())
            -- ^ the callback function. The first argument is the decoded
            --   arguments structure. The callback is passed a continuation
            --   which takes a possible reply structure.
            -> a  -- ^ dummy value needed for type inference, can be undefined
            -> b  -- ^ dummy value needed for type inference, can be undefined
            -> Callback
rpcFunction handler _ _ request payload cb = do
  let request = Tagged.deserialise payload
  case request of
       (Left _) -> atomically $ cb (rpcreplyEmpty { rpcreply_reply_code = fromIntegral $ fromEnum ErrPayloadParseFailed }) BS.empty
       (Right request) -> do
         let cb' Nothing = cb (rpcreplyEmpty { rpcreply_reply_code = 32 }) BS.empty
             cb' (Just reply) = cb rpcreplyEmpty $ Tagged.serialise reply
         handler request cb'

-- | Export a new service on a given Port
newService :: Port.Port  -- ^ the port to export the service on
           -> String  -- ^ the service name
           -> State  -- ^ initial state of the service
           -> DispatchTable  -- ^ information about the methods
           -> IO ()
newService port name initstate dispatcht = do
  st <- atomically $ newTVar initstate
  let serv = Service name st port dispatcht
  atomically $ Port.addService port name $ dispatch serv

dispatch :: Service -> Port.Callback
dispatch serv obreq payload cb = do
  state <- atomically $ readTVar $ servstate serv
  let good = case state of Up -> 1; otherwise -> 0
  let id = outboundrequest_id obreq

  case outboundrequest_probe obreq of
    Just _ -> atomically $ cb (inboundreplyEmpty { inboundreply_health = Just (healthreplyEmpty { healthreply_good = good}), inboundreply_id = id }) BS.empty
    Nothing -> return ()

  case outboundrequest_rpc obreq of
    Just rpcreq -> do
      let mmethod = Map.lookup (rpcrequest_method rpcreq) $ servcbs serv
      case mmethod of
        Nothing -> do
          -- return unknown method
          let replyCode = fromIntegral $ fromEnum ErrMethodUnknown
          atomically $ cb (inboundreplyEmpty { inboundreply_rpc = Just (rpcreplyEmpty { rpcreply_reply_code = replyCode }), inboundreply_id = id }) BS.empty
        Just method -> (methcallback method) rpcreq payload $ handleReply obreq cb
    Nothing -> return ()

-- | This is a callback from the service implementation which is called when an
--   RPC is finished and the reply is ready
handleReply :: Outboundrequest  -- ^ the request which resulted in this reply
            -> (Inboundreply -> BS.ByteString -> STM ())
               -- ^ the Port's callback action
            -> Rpcreply  -- ^ the method's reply
            -> BS.ByteString  -- ^ ... and payload
            -> STM ()
handleReply obreq cb rpcreply payload = do
  let ibreply = inboundreplyEmpty { inboundreply_id = outboundrequest_id obreq
                                  , inboundreply_rpc = Just rpcreply }
  cb ibreply payload