git.iko.soy/typeable/servant-reflex
on mniip/uverb-stuff help sign in
224 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
8API changes its input parameters or return type, XHR requests from the
9frontend will fail at runtime. If your API is defined by
10[servant](https://haskell-servant.readthedocs.io) combinators, you can use
11`servant-reflex` to share the API between the server and frontend.
12Syncronization between is checked at compile time, and rather than building XHR
13requests by hand, API endpoints are available behind `reflex`'s FRP semantics.
16## Example
18We have a webservice API defined in a module where both the server (compiled
19with ghc) and the frontend (compiled with ghcjs) can see it:
21```haskell
22type API = "getint" :> Get '[JSON] Int
23 :<|> "sayhi" :> QueryParam "username" Text
24 :> QueryParams "greetings" Text
25 :> QueryFlag "gusto"
26 :> Get '[JSON] Text
27 :<|> "double" :> ReqBody '[JSON] Double
28 :> Post '[JSON] Double
29 :<|> Raw
30```
32`servant-reflex` then computes client functions that can query the API through
33an `XhrRequest`.
35```haskell
37 runGUI :: forall t m.MonadWidget t m => do
39 -- servant-reflex computes FRP functions for each API endpoint
40 let (getint :<|> sayhi :<|> doubleit :<|> _) = client (Proxy :: Proxy API)
41 (Proxy :: Proxy m)
42 (Proxy :: Proxy ())
43 (constDyn (BasePath "/"))
44```
46These client functions are computed from your API type. They manage
47serialization, `XhrRequest` generation, and deserialization for you. `a`
48parameters used in URL captures become `Dynamic t (Either Text a)` parameters
49in the client functions. `QueryFlag`, `QueryParams` and `QueryParam` API
50parameters map to `Dynamic t Bool`, `Dynamic t [a]` and `Dynamic t (QParam a)`
51respectively. These parameters to the client function are wrapped with failure
52possibility to allow you to indicate at any time whether input validation for
53that parameter has failed and no valid XHR request can be generated. The final
54parameter is a trigger event for the XHR request. The return value
55`Event t (ReqResult a)` contains responses from the API server.
57```haskell
58 -- No need to write these functions. servant-reflex creates them for you!
59 getint :: MonadWidget t m
60 => Event t () -- ^ Trigger the XHR Request
61 -> m (Event t (ReqResult () Int)) -- ^ Consume the answer
63 sayhi :: MonadWidget t m
64 => Dynamic t (QParam Text)
65 -- ^ One input parameter - the 'name', wrapped in 'QParam'
66 -> Dynamic t [Text]
67 -- ^ Another input: list of preferred greetings
68 -> Dynamic t Bool
69 -- ^ Flag for capitalizing the response
70 -> Event t ()
71 -- ^ Trigger the XHR Request
72 -> m (Event t (ReqResult () Text))
74 doubleit :: MonadWidget t m
75 => Dynamic t (Either Text Double)
76 -> Event t ()
77 -> m (Event t (ReqResult () Double))
78```
80`ReqResult tag a` is defined in
81[`Servant.Common.Req`](https://github.com/imalsogreg/servant-reflex/blob/6d866e338edb9bf6fd8f8d5083ff0187b4d8c0d2/src/Servant/Common/Req.hs#L40-L42)
82and reports whether or not your request was sent (if validation fails, the
83request won't be sent), and how decoding of the response went. You can pattern
84match on these explicitly, but usually you'll want to use
85`fmapMaybe :: (a -> Maybe b) -> Event t a -> Event t b` and one of the
86elimination functions to filter the result type you care about, like this:
88```haskell
89 -- ... continued ...
90 res :: Event t (ReqResult Double) <- doubleIt xs triggers
91 let ys = fmapMaybe reqSuccess res
92 errs = fmapMaybe reqFailure res
94 -- Green <p> tag showing the last good result
95 elAttr "p" ("style" =: "color:green") $ do
96 text "Last good result: "
97 dynText =<< holdDyn "" (fmap show ys)
99 -- Red <p> tag showing the last error, cleared by a new good value
100 elAttr "p" ("style" =: "color:red") $
101 dynText =<< holdDyn "" (leftmost [errs, const "" <$> ys])
102```
104This example builds some input fields to enter API parameters, buttons to
105trigger the API calls, and text elements to show the results:
107```haskell
108 elClass "div" "int-demo" $ do
109 intButton <- button "Get Int"
110 serverInts <- fmapMaybe resSuccess <$> getint intButton
111 display =<< holdDyn (Just 0) serverInts
113 elClass "div" "hello-demo" $ do
114 nameText <- QParamSome . value <$> textInput def
115 greetings <- (fmap words . value) <$> textInput def
116 withGusto <- checkbox def
117 helloButton <- button "Say hi"
118 hellos <- fmapMaybe resResult <$> sayhi nameText greetings withGusto helloButton
119 display =<< holdDyn Nothing hellos
121 elClass "div" "demo-double" $ do
122 inputDouble <- (fmapMaybe readMaybe) <$> textInput def
123 doubleButton <- button "Double it"
124 outputDouble <- fmapMaybe resSuccess <$> doubleit inputDouble doubleButton
125 display =<< holdDyn Nothing outputDouble
126```
128For a great introduction to recative DOM building, see the
129[README](https://github.com/reflex-frp/reflex-platform) for the
130`reflex-platform`. For more information about servant, see their
131[documentation](http://haskell-servant.readthedocs.io/en/stable/). Thanks to
132the respective authors of these fabulous libraries.
135## Building the library and test server
137This repository comes with a small example of an API shared between a
138ghcjs-compiled frontend
139([exec/](https://github.com/imalsogreg/servant-reflex/tree/master/exec)) and a
140ghc-compiled backend
141([testserver/](https://github.com/imalsogreg/servant-reflex/tree/master/testserver).
142To build these components, we will use the nix packages for the testserver. It
143manages the frontend build as a dependency.
145```
146nix build -f travis.nix testserver -o server
147```
149Point it at the static directory and run it:
151```
152STATIC_DIR=server/static server/back -p 8000
153```
155Browse to [http://localhost:8000](http://localhost:8000) and click around.
156Open the firefox or chrome developer tools and examine the XHR activity
157under the Network tab.
159**For a larger example of a project that shares types between backend and
160frontend, see [hsnippet](https://github.com/mightybyte/hsnippet).**
163## Tagging requests
165The input and the return type of a client function like `getDouble` are both
166event streams. The individual input events and responses occur at different
167times and aren't automatically paired up, but you can recover the
168relationship by tagging the requests.
170So far we have used an `Event t ()` to trigger sending a request. If we choose
171e.g. `Double` for the third `Proxy` argument to `client`, then `Event t Double`
172will be used to trigger requests, and each `ReqResult` will carry the tag of
173its request. Imagine we wanted to display not just the last "double" from
174`doubleIt`, but a whole table of valid inputs and their doubled responses:
176```haskell
177 ...
178 inp <- textInput def
180 -- Convert the raw text into an `Either Text Double`
181 let inpNum = maybe (Left "No Parse") Right . readMaybe . T.unpack <$> value inp
182 go <- button "Double It"
184 -- Call 'doubleIt' with triggers that coincide with good input parses
185 rs <- doubleIt inpNum (fforMaybe (tagPromptlyDyn inpNum go) $ \case
186 Left _ -> Nothing
187 Right a -> Just a
188 )
190 -- Accumulate good responses in a map
191 doubleTable <- foldDyn (<>) mempty $ ffor rs $ \case
192 ResponseSuccess tag v _ -> tag =: v
193 _ -> mempty
195 el "table" $ do
196 listWithKey doubleTable $ \k dv -> el "tr" $ do
197 el "td" $ text (T.pack $ show k)
198 el "td" $ dynText (T.pack . show <$> dv)
199 ...
200```
202## Simultaneous requests
204`Servant.Reflex.Multi` provides an alternative client-generation function
205called `clientA` (client applicative). Choose a container type that has both
206`Applicative` and `Traversable` instances, and pass it to `clientA` through
207another `Proxy`. Our `sayHi` client function will then have this type:
209```haskell
210sayHi
211 :: Dynamic t (f (QParam Text))
212 -> Dynamic t (f [Text])
213 -> Dynamic t (f Bool)
214 -> Event t tag
215 -> m (Event t (f (ReqResult tag Text)))
216```
218The dynamic params are each wrapped in `f`. For every firing of the trigger
219event `tag`, all of these parameters will be combined according to `f`'s
220`Applicative` instance (when `f` is `[]`, you will get all combinations of
221all parameters taken together as a request; when `f` is `ZipList`, the Nth
222elemens of each parameters list will be taken together as a request). Using
223this interface, you can trigger many XHR's from a single event occurence, and
224expect the responses to be structured the same way as the requests.