remote-json-0.2: Control/Remote/Monad/JSON/Router.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-|
Module: Control.Remote.Monad.JSON.Router where
Copyright: (C) 2015, The University of Kansas
License: BSD-style (see the file LICENSE)
Maintainer: Justin Dawson
Stability: Alpha
Portability: GHC
-}
module Control.Remote.Monad.JSON.Router
( -- * The server RPC router
router
-- * The datatype that represents what we receive and what we dispatch
, ReceiveAPI(..)
, Call(..)
-- * Utilty methods
, transport
, methodNotFound
, invalidParams
, parseError
) where
import Control.Monad.Catch
import Control.Remote.Monad.JSON.Types
import Control.Natural
import Data.Aeson
import Data.Text(Text)
import Data.Typeable
import qualified Data.Vector as V
-- | 'Call' is a user-visable deep embedding of a method or notification call.
-- Server's provide transformations on this to implement remote-side call dispatching.
data Call :: * -> * where
CallMethod :: Text -> Args -> Call Value
CallNotification :: Text -> Args -> Call ()
-- | "The Server MAY process a batch rpc call as a set of concurrent tasks,
-- processing them in any order and with any width of parallelism."
-- We control this using the first argument.
router :: MonadCatch m
=> (forall a. [m a] -> m [a])
-> (Call :~> m) -> (ReceiveAPI :~> m)
router s f = nat $ \ case
(Receive v@(Object {})) -> simpleRouter f v
(Receive (Array a))
| V.null a -> return $ Just $ invalidRequest
| otherwise -> do
rs <- s (map (simpleRouter f) $ V.toList a)
case [ v | Just v <- rs ] of
[] -> return Nothing -- If there are no Response objects contained within the
-- Response array as it is to be sent to the client,
-- the server MUST NOT return an empty Array and should
-- return nothing at all.
vs -> return (Just (toJSON vs))
(Receive _) -> return $ Just $ invalidRequest
-- The simple router handle a single call.
simpleRouter :: forall m . MonadCatch m
=> (Call :~> m)
-> Value -> m (Maybe Value)
simpleRouter (Nat f) v = case call <$> fromJSON v of
Success m -> m
Error _ -> return $ Just $ invalidRequest
where
call :: JSONCall -> m (Maybe Value)
call (MethodCall (Method nm args) tag) = (do
r <- f (CallMethod nm args :: Call Value)
return $ Just $ object
[ "jsonrpc" .= ("2.0" :: Text)
, "result" .= toJSON r
, "id" .= tag
]) `catches`
[ Handler $ \ (_ :: MethodNotFound) ->
return $ Just $ toJSON
$ errorResponse (-32601) "Method not found" tag
, Handler $ \ (_ :: InvalidParams) ->
return $ Just $ toJSON
$ errorResponse (-32602) "Invalid params" tag
, Handler $ \ (_ :: SomeException) ->
return $ Just $ toJSON
$ errorResponse (-32603) "Internal error" tag
]
call (NotificationCall (Notification nm args)) =
(f (CallNotification nm args) >> return Nothing) `catchAll` \ _ -> return Nothing
-- | 'transport' connects the ability to recieve a message with the ability
-- to send a message. Typically this is done using TCP/IP and HTTP,
-- but we can simulate the connection here.
transport :: (Monad f) => (ReceiveAPI :~> f) -> (SendAPI :~> f)
transport f = nat $ \ case
Sync v -> do
r <- f # Receive v
case r of
Nothing -> fail "no result returned in transport"
Just v0 -> return v0
Async v -> do
r <- f # Receive v
case r of
Nothing -> return ()
Just v0 -> fail $ "unexpected result in transport: " ++ show v0
errorResponse :: Int -> Text -> Value -> Value
errorResponse code msg theId = toJSON $
ErrorResponse (ErrorMessage code msg) theId
invalidRequest :: Value
invalidRequest = errorResponse (-32600) "Invalid Request" Null
-- | For use when parsing to a JSON value fails inside a server,
-- before calling the router
parseError :: Value
parseError = errorResponse (-32700) "Parse error" Null
data MethodNotFound = MethodNotFound
deriving (Show, Typeable)
instance Exception MethodNotFound
-- | Throw this exception when a 'JSONCall a -> IO a' fails to match a method
-- or notification.
methodNotFound :: MonadThrow m => m a
methodNotFound = throwM $ MethodNotFound
data InvalidParams = InvalidParams
deriving (Show, Typeable)
instance Exception InvalidParams
-- | Throw this for when a 'JSONCall a -> IO a' method matches, but has invalid params.
invalidParams :: MonadThrow m => m a
invalidParams = throwM $ InvalidParams