Generate reflex client functions for querying a servant API
Go to file
2016-12-30 12:51:26 -05:00
deps Add servant-snap submodule back 2016-11-10 16:28:38 -08:00
exec Readme and build script cleanup 2016-09-21 10:54:26 -04:00
include add cpp 2016-02-08 21:09:34 -05:00
src/Servant add forgotten file 2016-12-30 12:51:26 -05:00
testdriver Cleanup cruft 2016-09-20 13:16:31 -04:00
testserver Start test server, selenium driver and testsuite from single script 2016-09-20 08:34:51 -04:00
.gitignore More tweaks to build script 2016-08-03 22:41:42 -04:00
.gitmodules Add servant-snap submodule back 2016-11-10 16:28:38 -08:00
.travis.yml travis 2016-11-10 17:15:12 -08:00
build.sh Fix travis 2016-11-10 16:54:22 -08:00
deploy_key.enc Add skeletal test framework and travis scripts to deploy test coverage 2016-08-03 20:03:00 -04:00
LICENSE.md Add BSD3 license. 2016-07-25 22:42:09 -04:00
overrides-ghc.nix Merge branch 'master' of github.com:imalsogreg/servant-reflex 2016-12-12 14:11:55 -08:00
overrides.nix Fix up readme type errors 2016-12-12 14:11:39 -08:00
README.md Fix up readme type errors 2016-12-12 14:11:39 -08:00
servant-reflex.cabal Stubby beginning for more low-level API 2016-12-30 12:49:00 -05:00

servant-reflex

Build Status

The problem servant-reflex solves

Keeping your frontend in sync with your API server can be difficult - when the API changes its input parameters or return type, XHR requests from the frontend will fail at runtime. If your API is defined by servant combinators, you can use servant-reflex to share the API between the server and frontend. Syncronization between is checked at compile time, and rather than building XHR requests by hand, API endpoints are available behind reflex's FRP semantics.

Example

We have a webservice API defined in a module where both the server (compiled with ghc) and the frontend (compiled with ghcjs) can see it:

type API = "getint"  :> Get '[JSON] Int
      :<|> "sayhi"   :> QueryParam  "username" Text
                     :> QueryParams "greetings" Text
                     :> QueryFlag   "gusto"
                     :> Get '[JSON] Text
      :<|> "double" :> ReqBody '[JSON] Double
                    :> Post '[JSON] Double
      :<|> Raw

servant-reflex then computes client functions that can query the API through an XhrRequest.


 runGUI :: forall t m.MonadWidget t m => do

  -- servant-reflex computes FRP functions for each API endpoint
  let (getint :<|> sayhi :<|> doubleit :<|> _) = client (Proxy :: Proxy API)
                                                        (Proxy :: Proxy m)
                                                        (constDyn (BasePath "/"))

These client functions are computed from your API type. They manage serialization, XhrRequest generation, and deserialization for you. a parameters used in URL captures become Dynamic t (Either Text a) parameters in the client functions. 'QueryFlag', 'QueryParams' and 'QueryParam' API parameters map to 'Dynamic t Bool', 'Dynamic t [a]' and 'Dynamic t (QParam a)' respectively. These parameters to the client function are wrapped with failure possibility to allow you to indicate at any time whether input validation for that parameter has failed and no valid XHR request can be generated. The final parameter is a trigger event for the XHR request. The return value Event t (ReqResult a) contains responses from the API server.

   -- No need to write these functions. servant-reflex creates them for you!
   getint :: MonadWidget t m
          => Event t ()  -- ^ Trigger the XHR Request
          -> m (Event t (ReqResult Int)) -- ^ Consume the answer

   sayhi :: MonadWidget t m
         => Dynamic t (QParam Text) 
            -- ^ One input parameter - the 'name', wrapped in 'QParam'
         -> Dynamic t [Text]
            -- ^ Another input: list of preferred greetings
         -> Dynamic t Bool
            -- ^ Flag for capitalizing the response
         -> Event t ()
            -- ^ Trigger the XHR Request
         -> m (Event t (ReqResult Text))

   doubleit :: MonadWidget t m
            => Dynamic t (Either Text Double)
            -> Event t ()
            -> m (Event t (ReqResult Double))

ReqResult a is defined in Servant.Common.Req and reports whether or not your request was sent (if validation fails, the request won't be sent), and how decoding of the response went. You can pattern match on these explicitly, but usually you'll want to use fmapMaybe :: (a -> Maybe b) -> Event t a -> Event t b and one of the elimination functions to filter the result type you care about, like this:

  -- ... continued ...
  res :: Event t (ReqResult Double) <- doubleIt xs triggers
  let ys   = fmapMaybe reqSuccess res
      errs = fmapMaybe reqFailure res
  
  -- Green <p> tag showing the last good result 
  elAttr "p" ("style" =: "color:green") $ do
    text "Last good result: "
    dynText =<< holdDyn "" (fmap show ys)
    
  -- Red <p> tag showing the last error, cleared by a new good value
  elAttr "p" ("style" =: "color:red") $
    dynText =<< holdDyn "" (leftmost [errs, const "" <$> ys])

This example builds some input fields to enter API parameters, buttons to trigger the API calls, and text elements to show the results:

  elClass "div" "int-demo" $ do
    intButton  <- button "Get Int"
    serverInts <- fmapMaybe resSuccess <$> getint intButton
    display =<< holdDyn (Just 0) serverInts

  elClass "div" "hello-demo" $ do
    nameText <- QParamSome . value <$> textInput def
    greetings <- (fmap words . value) <$> textInput def
    withGusto <- checkbox def
    helloButton <- button "Say hi"
    hellos <- fmapMaybe resResult <$> sayhi nameText greetings withGusto helloButton
    display =<< holdDyn Nothing hellos

  elClass "div" "demo-double" $ do
    inputDouble  <- (fmapMaybe readMaybe) <$> textInput def
    doubleButton <- button "Double it"
    outputDouble <- fmapMaybe resSuccess <$> doubleit inputDouble doubleButton
    display =<< holdDyn Nothing outputDouble

For a great introduction to recative DOM building, see the README for the reflex-platform. For more information about servant, see their documentation. Thanks to the respective authors of these fabulous libraries.

Building the library and test server

This repository comes with a small example of an API shared between a ghcjs-compiled frontend (exec/) and a ghc-compiled backend (testserver/. To build these components:

First build the library:

git submodule update --init --recursive
./build.sh

Then build the test server:

deps/reflex-platform/work-on ./overrides-ghc.nix ./testserver --command "cd testserver && cabal build"

Running the example site

The server must be run from the directory where static assets live:

cd testserver
dist/build/back/back -p 8001

And simply browse to localhost:8001

For a larger example of a project that shares types between backend and frontend, see hsnippet.