packages feed

pinch-0.4.0.0: src/Pinch/Server.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE RankNTypes                #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE StandaloneDeriving        #-}
{-# LANGUAGE TypeApplications          #-}
{-# LANGUAGE TypeFamilies              #-}

module Pinch.Server
  (
    -- * Thrift Server creation

    ThriftServer (..)
  , createServer
  , Handler(..)
  , Request (..)

    -- * Running a Thrift Server
  , runConnection
  , ThriftError (..)
  , Channel (..)
  , createChannel
  , createChannel1

    -- * Thrift Server context

    -- | The context can be used to pass data from the environment to the thrift server functions.
    -- For example, you could pass the remote host name to the server to use it for logging purposes.
  , Context
  , ContextItem
  , addToContext
  , lookupInContext

    -- * Middlewares
  , multiplex
  , ServiceName (..)
  , onError

    -- * Helper functions

    -- | Functions mostly useful for defining custom `ThriftServer`s.
  , mapRequestMessage
  , getRequestMessage
  , mkApplicationExceptionReply
  ) where

import           Control.Exception        (Exception, SomeException, catchJust,
                                           fromException, throwIO, try)
import           Data.Dynamic             (Dynamic (..), fromDynamic, toDyn)
import           Data.Proxy               (Proxy (..))
import           Data.Typeable            (TypeRep, Typeable, typeOf, typeRep)

import qualified Data.HashMap.Strict      as HM
import qualified Data.Text                as T

import           Pinch.Internal.Exception
import           Pinch.Internal.Message
import           Pinch.Internal.Pinchable
import           Pinch.Internal.RPC
import           Pinch.Internal.TType

import qualified Pinch.Transport          as T

-- | A single request to a thrift server.
data Request out where
  RCall :: !Message -> Request Message
  ROneway :: !Message -> Request ()

deriving instance Show (Request out)

-- | Map the message contained in the request.
mapRequestMessage :: (Message -> Message) -> Request o -> Request o
mapRequestMessage f (RCall m)   = RCall $ f m
mapRequestMessage f (ROneway m) = ROneway $ f m

-- | Extract the message contained in the request.
getRequestMessage :: Request o -> Message
getRequestMessage (RCall m)   = m
getRequestMessage (ROneway m) = m

-- | A `Thrift` server. Takes the context and the request as input and may produces a reply message.
newtype ThriftServer = ThriftServer { unThriftServer :: forall a . Context -> Request a -> IO a }

-- | Allows passing context information to a `ThriftServer`.
-- The context is indexed by type.
newtype Context = Context (HM.HashMap TypeRep Dynamic)

instance Semigroup Context where
  (Context a) <> (Context b) = Context $ a <> b

instance Monoid Context where
  mempty = Context mempty

class Typeable a => ContextItem a where

instance ContextItem ServiceName


-- | Adds a new item to the context. If an item with the same
-- type is already part of the context, it will be overwritten.
addToContext :: forall i . ContextItem i => i -> Context -> Context
addToContext i (Context m) =
  Context $ HM.insert (typeOf i) (toDyn i) m

-- | Lookup a value in the context.
lookupInContext :: forall i . ContextItem i => Context -> Maybe i
lookupInContext (Context m) = do
  x <- HM.lookup (typeRep (Proxy :: Proxy i)) m
  case fromDynamic @i x of
    Nothing -> error "Impossible!"
    Just y  -> pure y

-- | Create a handler for a request type.
data Handler where
  -- | Handle normal call requests. Must return a result.
  CallHandler :: (Pinchable c, Tag c ~ TStruct, Pinchable r, Tag r ~ TStruct) => (Context -> c -> IO r) -> Handler
  -- | Handle oneway requests. Cannot return any result.
  OnewayHandler :: (Pinchable c, Tag c ~ TStruct) => (Context -> c -> IO ()) -> Handler

-- | Creates a new thrift server processing requests with the function `f`.
--
-- By default, if processing a oneway call fails a haskell exception is thrown which will likely
-- terminate the guilty connection. You may use the `onError` combinator to handle this case
-- more gracefully.
createServer :: (T.Text -> Maybe Handler) -> ThriftServer
createServer f = ThriftServer $ \ctx req ->
  case req of
    RCall msg ->
      case f $ messageName msg of
        Just (CallHandler f') ->
          case runParser $ unpinch $ messagePayload msg of
            Right args -> do
              ret <- f' ctx args
              pure $ Message
                  { messageName = messageName msg
                  , messageType = Reply
                  , messageId   = messageId msg
                  , messagePayload = pinch ret
                  }
            Left err ->
              pure $ mkApplicationExceptionReply msg $
                ApplicationException ("Unable to parse service arguments: " <> T.pack err) InternalError

        Just (OnewayHandler _) ->
          pure $ mkApplicationExceptionReply msg $
            ApplicationException "Expected message type Oneway, got Call." InvalidMessageType
        Nothing ->
          pure $ mkApplicationExceptionReply msg $ ApplicationException "Unknown method name." WrongMethodName

    ROneway msg ->
      -- we cannot return errors to the client as it is a oneway call.
      -- Instead we just throw an exception, possible terminating
      -- the guilty connection.
      case f $ messageName msg of
        Just (OnewayHandler f') -> do
          case runParser $ unpinch $ messagePayload msg of
            Right args -> f' ctx args
            Left err   ->
              throwIO $ ApplicationException ("Unable to parse service arguments: " <> T.pack err) InternalError
        Just (CallHandler _) ->
          throwIO $ ApplicationException "Expected message type Call, got Oneway." InvalidMessageType
        Nothing ->
          throwIO $ ApplicationException "Unknown method name." WrongMethodName

-- | Multiplex multiple services into a single `ThriftServer`.
--
-- The service name is added to the `Context` and may be retrieved using `lookupInContext @ServiceName ctx`.
multiplex :: [(ServiceName, ThriftServer)] -> ThriftServer
multiplex services = ThriftServer $ \ctx req -> do
  case req of
    RCall msg -> go ctx req (pure . mkApplicationExceptionReply msg)
    -- we cannot send the exception back, because it is a oneway call
    -- instead let's just throw it and crash the server
    ROneway _ -> go ctx req throwIO
  where
    srvMap = HM.fromList services

    go :: Context -> Request a -> (ApplicationException -> IO a) -> IO a
    go ctx req onErr = do
      let (prefix, method) = T.span (/= ':') (messageName $ getRequestMessage req)
      let prefix' = ServiceName prefix
      let ctx' = addToContext prefix' ctx
      case prefix' `HM.lookup` srvMap of
        _ | T.null method -> onErr $ ApplicationException "Invalid method name, expecting a colon." WrongMethodName
        Just srv -> do

          reply <- unThriftServer srv ctx' $ mapRequestMessage (\msg -> msg { messageName = T.tail method }) req

          case req of
            ROneway _ -> pure ()
            RCall _   -> pure reply
        Nothing -> onErr $ ApplicationException ("No service with name " <> prefix <> " available.") UnknownMethod

-- | Add error handlers to a `ThriftServer`. Exceptions are caught and not re-thrown, but you may do
-- so by calling `ioThrow` yourself.
onError
  :: Exception e
  => (e -> Maybe a) -- ^ Select exceptions to handle.
  -> (a -> IO Message) -- ^ Error handler for normal method calls.
  -> (a -> IO ()) -- ^ Error handler for oneway calls.
  -> ThriftServer -> ThriftServer
onError sel callError onewayError srv = ThriftServer $
  \ctx req ->
    catchJust sel
      (unThriftServer srv ctx req)
      (\e -> do
        case req of
          RCall _   -> callError e
          ROneway _ -> onewayError e
      )

-- | Run a Thrift server for a single connection.
runConnection :: Context -> ThriftServer -> Channel -> IO ()
runConnection ctx srv chan = do
  msg <- readMessage chan
  case msg of
    T.RREOF -> pure ()
    T.RRFailure err -> do
      throwIO $ ThriftError $ T.pack err
    T.RRSuccess call -> do
      case messageType call of
        Call -> do
          r <- try $ unThriftServer srv ctx (RCall call)
          case r of
            -- if it is already an ApplicationException, we just send it back
            Left (e :: SomeException)
              | Just appEx <- fromException e -> writeMessage chan $ mkApplicationExceptionReply call appEx
            Left (e :: SomeException) -> writeMessage chan $ mkApplicationExceptionReply call $
              ApplicationException ("Could not process request: " <> (T.pack $ show e)) InternalError
            Right x -> writeMessage chan x
        Oneway -> do
          -- no matter what happens, we can never send back an error because the client is not listening for replies
          -- when doing a oneway calls...
          -- Let's just crash the connection in this case, to avoid silently swallowing errors.
          -- `onError` can be used to handle this more gracefully.
          unThriftServer srv ctx (ROneway call)
        -- the client must never send Reply/Exception messages.
        t -> throwIO $
          ApplicationException ("Expected call, got " <> (T.pack $ show t)) InvalidMessageType
      runConnection ctx srv chan

-- | Builds an exception reply given the corresponding request message.
mkApplicationExceptionReply :: Message -> ApplicationException -> Message
mkApplicationExceptionReply req ex = Message
  { messageName = messageName req
  , messageType = Exception
  , messageId = messageId req
  , messagePayload = pinch ex
  }