packages feed

fpco-api-1.0.4: src/library/FP/API/Run.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE TemplateHaskell       #-}

-- | Function for running arbitrary IDE API commands.

module FP.API.Run
  where

import           FP.API.Convert
import           FP.API.Types

import           Control.Exception
import           Control.Failure
import           Control.Monad.Extra
import           Control.Monad.Logger
import           Control.Monad.Reader
import           Control.Monad.Trans.Resource
import           Data.Aeson                   (decode, encode)
import           Data.ByteString.Lazy         (toChunks)
import           Data.Data
import           Data.IORef
import           Data.Monoid
import           Data.String
import           Data.Text                    (Text)
import qualified Data.Text                    as T
import           Data.Text.Encoding           (encodeUtf8)
import           Language.Fay.Yesod           (Returns (..))
import           Network.HTTP.Conduit
import           Network.HTTP.Types.Status
import           Prelude hiding (catch)
import           Texts.English

-- | Only used internally for encoding.
data Command = IdeCommand IdeCommand
  deriving Show

-- | Monad that can get info for sending commands.
class (Failure HttpException m,MonadIO m,Functor m) => MonadClient m where
  getClientConfig :: m ClientConfig

-- | Simple command configuration.
data ClientConfig = CC
  { ccUrl       :: String
  , ccToken     :: String
  , ccManager   :: Manager
  , ccCookie    :: IORef CookieJar
  , ccUserAgent :: Text
  }

data CommandException
  = CommandException Text
   deriving (Show,Typeable)
instance Exception CommandException

-- | Helpful simple client instance.
instance (Failure HttpException m,MonadIO m,Functor m) => MonadClient (ReaderT ClientConfig m) where
  getClientConfig = ask

-- | Run the given command.
runCommand :: (MonadLogger m,MonadClient m,Data a,Show a) => (Returns' a -> IdeCommand) -> m a
runCommand cmd = do
  CC{..} <- getClientConfig
  request <- parseUrl (ccUrl <> "/fay-command")
  jar <- io $ readIORef ccCookie
  let req = urlEncodedBody params $ setup ccToken request (Just jar) ccUserAgent
  $(logDebug) ("=> " <> trunc (T.pack (show (IdeCommand (cmd Returns)))))
  resp <- io (runResourceT (httpLbs req ccManager))
  io $ writeIORef ccCookie (responseCookieJar resp)
  case statusCode (responseStatus resp) of
    200 -> do
      case decode (responseBody resp) >>= readFromFay' of
        Nothing -> do $(logDebug) (errprefix <> "<= [unable to decode] " <> T.pack (show (responseBody resp)))
                      error $ "Unable to decode response: " ++ show (responseBody resp)
        Just result ->
          case result of
            Failure e -> do $(logDebug) (errprefix <> "<= " <> trunc e)
                            throw (CommandException e)
            Success a -> do $(logDebug) ("<= " <> trunc (T.pack (show a)))
                            return a
    code -> throw (CommandException ("Bad status code returned from client command: " <> T.pack (show code)))

  where params =
          [("json",mconcat (toChunks (encode (showToFay' (IdeCommand (cmd Returns))))))]
        setup token req jar agent =
          req { method = "POST"
              , requestHeaders = requestHeaders req ++
                                 [("Accept","application/json")
                                 ,("User-Agent",encodeUtf8 ("fpco-api:" <> agent))
                                 ,("authorization","token " <> fromString token)]
              , responseTimeout = Nothing
              , cookieJar = jar
              }
        trunc :: Text -> Text
        trunc = ellipsize 140
        errprefix = "Error from request: " <> T.pack (show (cmd Returns)) <> "\n"