Generate reflex client functions for querying a servant API
Go to file
2016-09-17 21:02:11 -04:00
deps Bump submodule dependencies 2016-09-17 21:02:11 -04:00
exec Remove MonadWidget, tighten imports 2016-08-29 22:20:44 -04:00
include add cpp 2016-02-08 21:09:34 -05:00
src/Servant Remove MonadWidget, tighten imports 2016-08-29 22:20:44 -04:00
test Add skeletal test framework and travis scripts to deploy test coverage 2016-08-03 20:03:00 -04:00
testserver Dynamic servant api, tagDyn to catch most recent parameters; update example 2016-07-19 12:25:58 -04:00
.gitignore More tweaks to build script 2016-08-03 22:41:42 -04:00
.gitmodules Bump submodule dependencies 2016-09-17 21:02:11 -04:00
.travis.yml Check for actual additions 2016-08-04 01:16:38 -04:00
build.sh Deps and build cleanup 2016-06-12 09:56:17 -04:00
default.nix Deps update for ghc8 2016-08-02 18:10:12 -04:00
deploy_key.enc Add skeletal test framework and travis scripts to deploy test coverage 2016-08-03 20:03:00 -04:00
deploy.sh Check for actual additions 2016-08-04 01:16:38 -04:00
init-sandbox.sh Reorganize testserver, update all deps to 'improved-base' 2016-02-07 16:46:20 -05:00
LICENSE.md Add BSD3 license. 2016-07-25 22:42:09 -04:00
overrides-ghc.nix Make functor-dynamic warnings go away - merge req headers 2016-08-29 18:05:06 -04:00
overrides.nix Make functor-dynamic warnings go away - merge req headers 2016-08-29 18:05:06 -04:00
README.md Udpate paraameter types in Readme to Dynamics 2016-08-02 21:36:51 -04:00
runTestsAndCoverage.sh Travis tweaks to find hpc output 2016-08-04 00:39:24 -04:00
servant-reflex.cabal Handle response headers 2016-08-29 21:22:00 -04:00
shell.sh Switch from try-reflex to most recent reflex-platform 2016-03-12 10:48:38 +08:00
toSite.sh Fix type errors and example client and server 2016-02-18 17:56:26 -05:00

Note: work in progress.

servant-reflex

Build Status

This library lets you automatically derive reflex-dom clients that query each endpoint of a servant webservice.

Building the library and test server

With reflex-platform:

First build the library:

git clone https://github.com/imalsogreg/servant-reflex
cd servant-reflex
git submodule update --init --recursive
./build.sh
./toSite.sh

(the toSite.sh script copies the generated javascript to the server's static assets directory)

Then build the test server (not in a nix-shell):

./init-sandbox.sh
cd testserver && cabal install --only-dep && cabal build && cd ..

Or, with a system-wide ghcjs

You will need a recent GHCJS installation, or use the reflex-platform. A snap server using servant-snap is provided for serving the api. Snap 1.0 is not yet on Hackage (and servant-snap is still experimental), but we bundle the sources as a git submodule. To install everything with cabal:

git clone https://github.com/imalsogreg/servant-reflex
cd servant-reflex
git submodule update --init --recursive
./init-sandbox.sh
cd testserver && cabal install --only-dep && cabal build && cd ..
cabal install --only-dep
cabal build
./toSite

The toSite.hs script copies the ghcjs-generated files into the server's static directory.

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

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" String
                     :> QueryParams "greetings" String
                     :> QueryFlag   "gusto"
                     :> Get '[JSON] String
      :<|> "double" :> ReqBody '[JSON] Double
                    :> Post '[JSON] Double
      :<|> Raw

servant-reflex then computes 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 the API and manage serialization, XhrRequest generation, and deserialization for you. a parameters become Dynamic t (Either String a) values. You provide a trigger event and receive an Event t (ReqResult a), with responses from the API server (which you would write with servant-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 (Either String String) 
            -- ^ One input parameter - the 'name'
         -> Dynamic t [String]
            -- ^ Another input: list of preferred greetings
         -> Dynamic t Bool
            -- ^ Flag for capitalizing the response
         -> Event t ()
            -- ^ Trigger the XHR Request
         -> m (Event t (ReqResult String))

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

Plug any of these functions into your reflex frontend to consume backend servant services. Isomorphic!

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 <- 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.

TODOs

  • Request Headers
  • Tests
  • Code cleanup