/reflex-servant

Access Servant APIs using the Reflex Functional Reactive Programming library

Primary LanguageHaskellBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

reflex-servant lets you access Servant APIs with Reflex FRP.

  • API calls are as simple as Event in -> m (Event out).
  • Minimal dependencies, covered by just reflex and servant-client-core.
  • Customizable product types
  • traverseEndpoint for labeled events or sequential calls

Quick Start

Let's start with some pragma's and imports.

{-# LANGUAGE ConstraintKinds, DataKinds, FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings, RankNTypes, ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications, TypeOperators #-}
import Control.Monad
import Control.Monad.IO.Class
import Data.Proxy
import Data.Text(Text)
import Reflex
import Reflex.Servant
import Servant.API
import Servant.Client.Core

... and an example API:

type Api = "ping" :> Get '[JSON] Text
      :<|> "calendar" :> Capture "calendarName" Text :> "items" :> Get '[JSON] [(Timestamp, Text)]
      :<|> "calendar" :> Capture "calendarName" Text :> "items" :> ReqBody '[JSON] (Timestamp, Text) :> Post '[JSON] [(Timestamp, Text)]

-- not important
type Timestamp = Int

Now let's build a simple network. We'll pretend to use reflex-dom.

myApp :: forall t m. MonadWidget t m => m ()
myApp = do
  postBuild <- getPostBuild
  let ping :<|> list :<|> post = reflexClient (basicConfig myRunner) (Proxy @Api)
  calendarResponse <- list ("Birthday calendar" <$ postBuild)
  calendar <- holdDyn [] (filterRight calendarResponse)
  showCalendar calendar
  
showCalendar :: MonadWidget t m => Dynamic t [(Timestamp, Text)] -> m ()
showCalendar = void . dyn . fmap (undefined :: [(Timestamp, Text)] -> m ())

The runner is provided by a library, such as servant-client or servant-client-jsaddle.

Here's what the runner types look like.

myRunner :: ServantClientRunner () m
myRunner cfg (GenericClientM m) = myRunServantClient cfg m

myRunServantClient :: () -> (RunClient m' => m' a) -> m (Either ServantError a)
myRunServantClient cfg m = error "Call servant-client or servant-client-jsaddle"

The redundant cfg :: () is useful when calling multiple services. You can ignore it for now.

Advanced use

While the above approach is easy, it is not quite as flexible. If you want to use your client functions in a transformed monad, or if you're not comfortable passing the client functions as arguments, you can use a different configuration.

pingCalendar :<|> listCalendar :<|> postCalendar =
  reflexClient (calendarConfig) (Proxy @Api)

calendarConfig = (defaultConfig myRunner)
  { configEndpoint = ConfiguredEndpointConfig ()
  }

myApp' :: forall t m. MonadWidget t m => m ()
myApp' = do
  postBuild <- getPostBuild
  calendarResponse <- endpoint' listCalendar ((,) "Birthday calendar" <$> postBuild)
  calendar <- holdDyn [] (filterRight calendarResponse)
  showCalendar calendar

endpoint'
    :: (PerformEvent t m, TriggerEvent t m, MonadIO (Performable m))
    => Endpoint () i o
    -> Event t i
    -> m (Event t (Either ServantError o))
endpoint' = endpoint myRunner

If your application needs to access multiple APIs, you can make use of a configuration argument to distinguish between APIs.

data Service = CalendarService | OtherService

pingCalendar' :<|> listCalendar' :<|> postCalendar' =
  reflexClient (calendarConfig') (Proxy @Api)

calendarConfig' = (defaultConfig myRunner)
  { configEndpoint = ConfiguredEndpointConfig CalendarService
  }

endpoint''
    :: (PerformEvent t m, TriggerEvent t m, MonadIO (Performable m))
    => Endpoint Service i o
    -> Event t i
    -> m (Event t (Either ServantError o))
endpoint'' = endpoint multiRunner

multiRunner
    :: Service
    -> GenericClientM a
    -> m (Either ServantError a)
multiRunner CalendarService = myRunner ()
multiRunner OtherService = error "Other service runner not configured yet."

Of course variations are possible. Instead of a closed Service type, you may want to pass something more flexible, such as the runner itself, or you can use an open union if you want to substitute different runners for testing.

Literate Haskell

This is a literate readme, so you can actually typecheck and run this document!

main = pure ()

Host-independent

To illustrate the use of reflex-servant we have pretended to use reflex-dom, but actually this library does not depend on any particular reflex host. These are the fake definitions use to typecheck this document.

type MonadWidget t m = (Reflex t, MonadHold t m, MonadSample t (Performable m), PerformEvent t m, TriggerEvent t m, MonadIO (Performable m), PostBuild t m)

dyn :: MonadWidget t m => Dynamic t (m a) -> m (Event t a)
dyn = undefined

simpleTextInput :: MonadWidget t m => m (Dynamic t Text)
simpleTextInput = undefined

datePicker :: MonadWidget t m => m (Dynamic t Timestamp)
datePicker = undefined