git.iko.soy/typeable/servant-reflex
on cps-api help sign in
187 lines 9.0 kB history raw
1# servant-reflex
3[![Build Status](https://travis-ci.org/imalsogreg/servant-reflex.svg?branch=master)](https://travis-ci.org/imalsogreg/servant-reflex)
5## `servant-reflex` lets you share your `servant` APIs with the frontend
7Keeping 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](haskell-servant.readthedocs.io) combinators, you can use `servant-reflex` to share the API between the server and frontend.
8Syncronization between is checked at compile time, and rather than building XHR requests by hand, API endpoints are available behind `reflex`'s FRP semantics.
12## Example
14We have a webservice API defined in a module where both the server (compiled with ghc) and the frontend (compiled with ghcjs) can see it:
16```haskell
17type API = "getint" :> Get '[JSON] Int
18 :<|> "sayhi" :> QueryParam "username" Text
19 :> QueryParams "greetings" Text
20 :> QueryFlag "gusto"
21 :> Get '[JSON] Text
22 :<|> "double" :> ReqBody '[JSON] Double
23 :> Post '[JSON] Double
24 :<|> Raw
25```
27`servant-reflex` then computes client functions that can query the API through an `XhrRequest`.
29```haskell
31 runGUI :: forall t m.MonadWidget t m => do
33 -- servant-reflex computes FRP functions for each API endpoint
34 let (getint :<|> sayhi :<|> doubleit :<|> _) = client (Proxy :: Proxy API)
35 (Proxy :: Proxy m)
36 (Proxy :: Proxy ())
37 (constDyn (BasePath "/"))
38```
40These 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.
42```haskell
43 -- No need to write these functions. servant-reflex creates them for you!
44 getint :: MonadWidget t m
45 => Event t () -- ^ Trigger the XHR Request
46 -> m (Event t (ReqResult () Int)) -- ^ Consume the answer
48 sayhi :: MonadWidget t m
49 => Dynamic t (QParam Text)
50 -- ^ One input parameter - the 'name', wrapped in 'QParam'
51 -> Dynamic t [Text]
52 -- ^ Another input: list of preferred greetings
53 -> Dynamic t Bool
54 -- ^ Flag for capitalizing the response
55 -> Event t ()
56 -- ^ Trigger the XHR Request
57 -> m (Event t (ReqResult () Text))
59 doubleit :: MonadWidget t m
60 => Dynamic t (Either Text Double)
61 -> Event t ()
62 -> m (Event t (ReqResult () Double))
63```
65`ReqResult tag a` is defined in [`Servant.Common.Req`](https://github.com/imalsogreg/servant-reflex/blob/6d866e338edb9bf6fd8f8d5083ff0187b4d8c0d2/src/Servant/Common/Req.hs#L40-L42) 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:
67```haskell
68 -- ... continued ...
69 res :: Event t (ReqResult Double) <- doubleIt xs triggers
70 let ys = fmapMaybe reqSuccess res
71 errs = fmapMaybe reqFailure res
73 -- Green <p> tag showing the last good result
74 elAttr "p" ("style" =: "color:green") $ do
75 text "Last good result: "
76 dynText =<< holdDyn "" (fmap show ys)
78 -- Red <p> tag showing the last error, cleared by a new good value
79 elAttr "p" ("style" =: "color:red") $
80 dynText =<< holdDyn "" (leftmost [errs, const "" <$> ys])
81```
83This example builds some input fields to enter API parameters, buttons to trigger the API calls, and text elements to show the results:
85```haskell
86 elClass "div" "int-demo" $ do
87 intButton <- button "Get Int"
88 serverInts <- fmapMaybe resSuccess <$> getint intButton
89 display =<< holdDyn (Just 0) serverInts
91 elClass "div" "hello-demo" $ do
92 nameText <- QParamSome . value <$> textInput def
93 greetings <- (fmap words . value) <$> textInput def
94 withGusto <- checkbox def
95 helloButton <- button "Say hi"
96 hellos <- fmapMaybe resResult <$> sayhi nameText greetings withGusto helloButton
97 display =<< holdDyn Nothing hellos
99 elClass "div" "demo-double" $ do
100 inputDouble <- (fmapMaybe readMaybe) <$> textInput def
101 doubleButton <- button "Double it"
102 outputDouble <- fmapMaybe resSuccess <$> doubleit inputDouble doubleButton
103 display =<< holdDyn Nothing outputDouble
104```
106For a great introduction to recative DOM building, see the [README](https://github.com/reflex-frp/reflex-platform) for the `reflex-platform`. For more information about servant, see their [documentation](http://haskell-servant.readthedocs.io/en/stable/). Thanks to the respective authors of these fabulous libraries.
109## Building the library and test server
111This repository comes with a small example of an API shared between a ghcjs-compiled frontend ([exec/](https://github.com/imalsogreg/servant-reflex/tree/master/exec)) and a ghc-compiled backend ([testserver/](https://github.com/imalsogreg/servant-reflex/tree/master/testserver). To build these components:
114First build the library:
116```
117git submodule update --init --recursive
118./build.sh
119```
121Then build the test server:
123```
124deps/reflex-platform/work-on ./overrides-ghc.nix ./testserver --command "cd testserver && cabal build"
125```
128## Running the example site
130The server must be run from the directory where static assets live:
132```
133cd testserver
134dist/build/back/back -p 8001
135```
137And simply browse to `localhost:8001`
139**For a larger example of a project that shares types between backend and frontend, see [hsnippet](https://github.com/mightybyte/hsnippet).**
142## Tagging requests
144The input and the return type of a client function like `getDouble` are both event streams. The individual input events and responses occur at different times and aren't automatically paired up, but you can recover the relationship by tagging the requests.
146So far we have used an `Event t ()` to trigger sending a request. If we choose e.g. `Double` for the third `Proxy` argument to `client`, then `Event t Double` will be used to trigger requests, and each `ReqResult` will carry the tag of its request. Imagine we wanted to display not just the last "double" from `doubleIt`, but a whole table of valid inputs and their doubled responses:
148```haskell
149 ...
150 inp <- textInput def
152 -- Convert the raw text into an `Either Text Double`
153 let inpNum = maybe (Left "No Parse") Right . readMaybe . T.unpack <$> value inp
154 go <- button "Double It"
156 -- Call 'doubleIt' with triggers that coincide with good input parses
157 rs <- doubleIt inpNum (fforMaybe (tagPromptlyDyn inpNum go) $ \case
158 Left _ -> Nothing
159 Right a -> Just a
160 )
162 -- Accumulate good responses in a map
163 doubleTable <- foldDyn (<>) mempty $ ffor rs $ \case
164 ResponseSuccess tag v _ -> tag =: v
165 _ -> mempty
167 el "table" $ do
168 listWithKey doubleTable $ \k dv -> el "tr" $ do
169 el "td" $ text (T.pack $ show k)
170 el "td" $ dynText (T.pack . show <$> dv)
171 ...
172```
174## Simultaneous requests
176'Servant.Reflex.Multi' provides an alternative client-generation function called 'clientA' (client applicative). Choose a container type that has both `Applicative` and `Traversable` instances, and pass it to `clientA` through another 'Proxy'. Our `sayHi` client function will then have this type:
178```haskell
179sayHi
180 :: Dynamic t (f (QParam Text))
181 -> Dynamic t (f [Text])
182 -> Dynamic t (f Bool)
183 -> Event t tag
184 -> m (Event t (f (ReqResult tag Text)))
185```
187The dynamic params are each wrapped in `f`. For every firing of the trigger event `tag`, all of these parameters will be combined according to `f`'s applicative instance (when `f` is `[]`, you will get all combinations of all parameters taken together as a request; when `f` is 'ZipList', the Nth elemens of each parameters list will be taken together as a request). Using this interface, you can trigger many XHR's from a single event occurence, and expect the responses to be structured the same way as the requests.