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
reflexandservant-client-core. - Customizable product types
traverseEndpointfor labeled events or sequential calls
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 = IntNow 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.
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 myRunnerIf 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.
This is a literate readme, so you can actually typecheck and run this document!
main = pure ()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