@hackage dormouse-client0.1.0.0

Simple, type-safe and testable HTTP client

Dormouse-Client

Dormouse is an HTTP client that will help you REST.

It was designed with the following objectives in mind:

  • HTTP requests and responses should be modelled by a simple, immutable Haskell Record.
  • Real HTTP calls should be made via an abstraction layer (MonadDormouseClient) so testing and mocking is painless.
  • Illegal requests should be unrepresentable, such as HTTP GET requests with a content body.
  • It should be possible to enforce a protocol (e.g. https) at the type level.
  • It should be possible to handle large request and response bodies via constant memory streaming.

Example use:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

import Control.Monad.IO.Class
import Data.Aeson.TH 
import Dormouse.Client
import GHC.Generics (Generic)
import Dormouse.Url.QQ

data UserDetails = UserDetails 
  { name :: String
  , nickname :: String
  , email :: String
  } deriving (Eq, Show, Generic)

deriveJSON defaultOptions ''UserDetails

data EchoedJson a = EchoedJson 
  { echoedjson :: a
  } deriving (Eq, Show, Generic)

deriveJSON defaultOptions {fieldLabelModifier = drop 6} ''EchoedJson

main :: IO ()
main = do
  manager <- newManager tlsManagerSettings
  runDormouseClient (DormouseClientConfig { clientManager = manager }) $ do
    let 
      userDetails = UserDetails 
        { name = "James T. Kirk"
        , nickname = "Jim"
        , email = "james.t.kirk@starfleet.com"
        }
      req = accept json $ supplyBody json userDetails $ post [https|https://postman-echo.com/post|]
    response :: HttpResponse (EchoedJson UserDetails) <- expect req
    liftIO $ print response
    return ()

Building requests

GET requests

Building a GET request is simple using a Url (Please see the Dormouse-Uri documentation for more details of how to safely create and construct Urls).

postmanEchoGetUrl :: Url "http"
postmanEchoGetUrl = [http|http://postman-echo.com/get?foo1=bar1&foo2=bar2/|]

postmanEchoGetReq :: HttpRequest (Url "http") "GET" Empty EmptyPayload acceptTag
postmanEchoGetReq = get postmanEchoGetUrl

It is often useful to tell Dormouse about the expected Content-Type of the response in advance so that the correct Accept headers can be sent:

postmanEchoGetReq' :: HttpRequest (Url "http") "GET" Empty EmptyPayload acceptTag
postmanEchoGetReq' = accept json $ get postmanEchoGetUrl

POST requests

You can build POST requests in the same way

postmanEchoPostUrl :: Url "https"
postmanEchoPostUrl = [https|https://postman-echo.com/post|]

postmanEchoPostReq :: HttpRequest (Url "https") "POST" Empty EmptyPayload JsonPayload
postmanEchoPostReq = accept json $ post postmanEchoPostUrl

Expecting a response

Since we're expecting json, we also need data types and FromJSON instances to interpret the response with. Let's start with an example to handle the GET request.

{-# LANGUAGE DeriveGeneric #-}
data Args = Args 
  { foo1 :: String
  , foo2 :: String
  } deriving (Eq, Show, Generic)

data PostmanEchoResponse = PostmanEchoResponse
  { args :: Args
  } deriving (Eq, Show, Generic)

Once the request has been built, you can send it and expect a response of a particular type in any MonadDormouseClient m.

sendPostmanEchoGetReq :: MonadDormouseClient m => m PostmanEchoResponse
sendPostmanEchoGetReq = do
  (resp :: HttpResponse PostmanEchoResponse) <- expect postmanEchoGetReq'
  return $ responseBody resp

Running Dormouse

Dormouse is not opinionated about how you run it.

You can use a concrete type.

main :: IO ()
main = do
  manager <- newManager tlsManagerSettings
  postmanResponse <- runDormouseClient (DormouseClientConfig { clientManager = manager }) sendPostmanEchoGetReq
  print postmanResponse

You can integrate the DormouseClientT Monad Transformer into your transformer stack.

main :: IO ()
main = do
  manager <- newManager tlsManagerSettings
  postmanResponse <- runDormouseClientT (DormouseClientConfig { clientManager = manager }) sendPostmanEchoGetReq
  print postmanResponse

You can also integrate into your own Application monad using the sendHttp function from Dormouse.Client.MonadIOImpl and by providing an instance of HasDormouseConfig for your application environment.

data MyEnv = MyEnv 
  { dormouseEnv :: DormouseClientConfig
  }

instance HasDormouseClientConfigMyEnv where
  getDormouseClientConfig = dormouseEnv

newtype AppM a = AppM
  { unAppM :: ReaderT Env IO a 
  } deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO, MonadThrow)

instance MonadDormouseClient (AppM) where
  send = IOImpl.sendHttp

runAppM :: Env -> AppM a -> IO a
runAppM deps app = flip runReaderT deps $ unAppM app