git.iko.soy/typeable/servant
on release-0.11 help sign in
typeable/servant/doc/tutorial/Javascript.lhs
521 lines 13 kB history raw
1# Generating Javascript functions to query an API
3We will now see how **servant** lets you turn an API type into javascript
4functions that you can call to query a webservice.
6For this, we will consider a simple page divided in two parts. At the top, we
7will have a search box that lets us search in a list of Haskell books by
8author/title with a list of results that gets updated every time we enter or
9remove a character, while at the bottom we will be able to see the classical
10[probabilistic method to approximate
11pi](http://en.wikipedia.org/wiki/Approximations_of_%CF%80#Summing_a_circle.27s_area),
12using a webservice to get random points. Finally, we will serve an HTML file
13along with a couple of Javascript files, among which one that's automatically
14generated from the API type and which will provide ready-to-use functions to
15query your API.
17The source for this tutorial section is a literate haskell file, so first we
18need to have some language extensions and imports:
20``` haskell
21{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
23{-# LANGUAGE OverloadedStrings #-}
24{-# LANGUAGE TypeOperators #-}
26module Javascript where
28import Control.Monad.IO.Class
29import Data.Aeson
30import Data.Proxy
31import Data.Text as T (Text)
32import Data.Text.IO as T (writeFile, readFile)
33import GHC.Generics
34import Language.Javascript.JQuery
35import Network.Wai
36import Network.Wai.Handler.Warp
37import qualified Data.Text as T
38import Servant
39import Servant.JS
40import System.Random
41```
43Now let's have the API type(s) and the accompanying datatypes.
45``` haskell
46type API = "point" :> Get '[JSON] Point
47 :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book)
49type API' = API :<|> Raw
51data Point = Point
52 { x :: Double
53 , y :: Double
54 } deriving Generic
56instance ToJSON Point
58data Search a = Search
59 { query :: Text
60 , results :: [a]
61 } deriving Generic
63mkSearch :: Text -> [a] -> Search a
64mkSearch = Search
66instance ToJSON a => ToJSON (Search a)
68data Book = Book
69 { author :: Text
70 , title :: Text
71 , year :: Int
72 } deriving Generic
74instance ToJSON Book
76book :: Text -> Text -> Int -> Book
77book = Book
78```
80We need a "book database". For the purpose of this guide, let's restrict
81ourselves to the following books.
83``` haskell
84books :: [Book]
85books =
86 [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000
87 , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008
88 , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011
89 , book "Graham Hutton" "Programming in Haskell" 2007
90 , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013
91 , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998
92 ]
93```
95Now, given an optional search string `q`, we want to perform a case insensitive
96search in that list of books. We're obviously not going to try and implement
97the best possible algorithm, this is out of scope for this tutorial. The
98following simple linear scan will do, given how small our list is.
100``` haskell
101searchBook :: Monad m => Maybe Text -> m (Search Book)
102searchBook Nothing = return (mkSearch "" books)
103searchBook (Just q) = return (mkSearch q books')
105 where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b)
106 || q' `T.isInfixOf` T.toLower (title b)
107 )
108 books
109 q' = T.toLower q
110```
112We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y
113<= 1`. The code below uses
114[random](http://hackage.haskell.org/package/random)'s `System.Random`.
116``` haskell
117randomPoint :: MonadIO m => m Point
118randomPoint = liftIO . getStdRandom $ \g ->
119 let (rx, g') = randomR (-1, 1) g
120 (ry, g'') = randomR (-1, 1) g'
121 in (Point rx ry, g'')
122```
124If we add static file serving, our server is now complete.
126``` haskell
127api :: Proxy API
128api = Proxy
130api' :: Proxy API'
131api' = Proxy
133server :: Server API
134server = randomPoint
135 :<|> searchBook
137server' :: Server API'
138server' = server
139 :<|> serveDirectoryFileServer "static"
141app :: Application
142app = serve api' server'
144main :: IO ()
145main = run 8000 app
146```
148Why two different API types, proxies and servers though? Simply because we
149don't want to generate javascript functions for the `Raw` part of our API type,
150so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
152The `EmptyAPI` combinator needs no special treatment as it generates no
153Javascript functions: an empty API has no endpoints to access.
155Very similarly to how one can derive haskell functions, we can derive the
156javascript with just a simple function call to `jsForAPI` from
157`Servant.JS`.
159``` haskell
160apiJS1 :: Text
161apiJS1 = jsForAPI api jquery
162```
164This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
166``` javascript
168var getPoint = function(onSuccess, onError)
170 $.ajax(
171 { url: '/point'
172 , success: onSuccess
173 , error: onError
174 , type: 'GET'
175 });
178var getBooks = function(q, onSuccess, onError)
180 $.ajax(
181 { url: '/books' + '?q=' + encodeURIComponent(q)
182 , success: onSuccess
183 , error: onError
184 , type: 'GET'
185 });
187```
189We created a directory `static` that contains two static files: `index.html`,
190which is the entrypoint to our little web application; and `ui.js`, which
191contains some hand-written javascript. This javascript code assumes the two
192generated functions `getPoint` and `getBooks` in scope. Therefore we need to
193write the generated javascript into a file:
195``` haskell
196writeJSFiles :: IO ()
197writeJSFiles = do
198 T.writeFile "static/api.js" apiJS1
199 jq <- T.readFile =<< Language.Javascript.JQuery.file
200 T.writeFile "static/jq.js" jq
201```
203(We're also writing the jquery library into a file, as it's also used by
204`ui.js`.) `static/api.js` will be included in `index.html` and the two
205generated functions will therefore be available in `ui.js`.
207And we're good to go. You can start the `main` function of this file and go to
208`http://localhost:8000/`. Start typing in the name of one of the authors in our
209database or part of a book title, and check out how long it takes to
210approximate pi using the method mentioned above.
212## Customizations
214Instead of calling `jquery`, you can call its variant `jqueryWith`.
215Here are the type definitions
217```haskell ignore
218jquery :: JavaScriptGenerator
219jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator
220```
222The `CommonGeneratorOptions` will let you define different behaviors to
223change how functions are generated. Here is the definition of currently
224available options:
226```haskell ignore
227data CommonGeneratorOptions = CommonGeneratorOptions
228 {
229 -- | function generating function names
230 functionNameBuilder :: FunctionName -> Text
231 -- | name used when a user want to send the request body (to let you redefine it)
232 , requestBody :: Text
233 -- | name of the callback parameter when the request was successful
234 , successCallback :: Text
235 -- | name of the callback parameter when the request reported an error
236 , errorCallback :: Text
237 -- | namespace on which we define the js function (empty mean local var)
238 , moduleName :: Text
239 -- | a prefix that should be prepended to the URL in the generated JS
240 , urlPrefix :: Text
241 }
242```
244This pattern is available with all supported backends, and default values are provided.
246## Vanilla support
248If you don't use JQuery for your application, you can reduce your
249dependencies to simply use the `XMLHttpRequest` object from the standard API.
251Use the same code as before but simply replace the previous `apiJS` with
252the following one:
254``` haskell
255apiJS2 :: Text
256apiJS2 = jsForAPI api vanillaJS
257```
259The rest is *completely* unchanged.
261The output file is a bit different, but it has the same parameters,
263``` javascript
266var getPoint = function(onSuccess, onError)
268 var xhr = new XMLHttpRequest();
269 xhr.open('GET', '/point', true);
270 xhr.setRequestHeader(\"Accept\",\"application/json\");
271 xhr.onreadystatechange = function (e) {
272 if (xhr.readyState == 4) {
273 if (xhr.status == 204 || xhr.status == 205) {
274 onSuccess();
275 } else if (xhr.status >= 200 && xhr.status < 300) {
276 var value = JSON.parse(xhr.responseText);
277 onSuccess(value);
278 } else {
279 var value = JSON.parse(xhr.responseText);
280 onError(value);
281 }
282 }
283 }
284 xhr.send(null);
287var getBooks = function(q, onSuccess, onError)
289 var xhr = new XMLHttpRequest();
290 xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
291 xhr.setRequestHeader(\"Accept\",\"application/json\");
292 xhr.onreadystatechange = function (e) {
293 if (xhr.readyState == 4) {
294 if (xhr.status == 204 || xhr.status == 205) {
295 onSuccess();
296 } else if (xhr.status >= 200 && xhr.status < 300) {
297 var value = JSON.parse(xhr.responseText);
298 onSuccess(value);
299 } else {
300 var value = JSON.parse(xhr.responseText);
301 onError(value);
302 }
303 }
304 }
305 xhr.send(null);
309```
311And that's all, your web service can of course be accessible from those
312two clients at the same time!
314## Axios support
316### Simple usage
318If you use Axios library for your application, we support that too!
320Use the same code as before but simply replace the previous `apiJS` with
321the following one:
323``` haskell
324apiJS3 :: Text
325apiJS3 = jsForAPI api $ axios defAxiosOptions
326```
328The rest is *completely* unchanged.
330The output file is a bit different,
332``` javascript
335var getPoint = function()
337 return axios({ url: '/point'
338 , method: 'get'
339 });
344var getBooks = function(q)
346 return axios({ url: '/books' + '?q=' + encodeURIComponent(q)
347 , method: 'get'
348 });
351```
353**Caution:** In order to support the promise style of the API, there are no onSuccess
354nor onError callback functions.
356### Defining Axios configuration
358Axios lets you define a 'configuration' to determine the behavior of the
359program when the AJAX request is sent.
361We mapped this into a configuration
363``` haskell
364data AxiosOptions = AxiosOptions
365 { -- | indicates whether or not cross-site Access-Control requests
366 -- should be made using credentials
367 withCredentials :: !Bool
368 -- | the name of the cookie to use as a value for xsrf token
369 , xsrfCookieName :: !(Maybe Text)
370 -- | the name of the header to use as a value for xsrf token
371 , xsrfHeaderName :: !(Maybe Text)
372 }
373```
375## Angular support
377### Simple usage
379You can apply the same procedure as with `vanillaJS` and `jquery`, and
380generate top level functions.
382The difference is that `angular` Generator always takes an argument.
384``` haskell
385apiJS4 :: Text
386apiJS4 = jsForAPI api $ angular defAngularOptions
387```
389The generated code will be a bit different than previous generators. An extra
390argument `$http` will be added to let Angular magical Dependency Injector
391operate.
393**Caution:** In order to support the promise style of the API, there are no onSuccess
394nor onError callback functions.
396``` javascript
399var getPoint = function($http)
401 return $http(
402 { url: '/point'
403 , method: 'GET'
404 });
409var getBooks = function($http, q)
411 return $http(
412 { url: '/books' + '?q=' + encodeURIComponent(q)
413 , method: 'GET'
414 });
417```
419You can then build your controllers easily
421``` javascript
423app.controller("MyController", function($http) {
424 this.getPoint = getPoint($http)
425 .success(/* Do something */)
426 .error(/* Report error */);
428 this.getPoint = getBooks($http, q)
429 .success(/* Do something */)
430 .error(/* Report error */);
431});
432```
434### Service generator
436You can also generate automatically a service to wrap the whole API as
437a single Angular service:
439``` javascript
440app.service('MyService', function($http) {
441 return ({
442 postCounter: function()
443 {
444 return $http(
445 { url: '/counter'
446 , method: 'POST'
447 });
448 },
449 getCounter: function()
450 {
451 return $http(
452 { url: '/books' + '?q=' + encodeURIComponent(q), true);
453 , method: 'GET'
454 });
455 }
456 });
457});
458```
460To do so, you just have to use an alternate generator.
462``` haskell
463apiJS5 :: Text
464apiJS5 = jsForAPI api $ angularService defAngularOptions
465```
467Again, it is possible to customize some portions with the options.
469``` haskell
470data AngularOptions = AngularOptions
471 { -- | When generating code with wrapInService, name of the service to generate, default is 'app'
472 serviceName :: Text
473 , -- | beginning of the service definition
474 prologue :: Text -> Text -> Text
475 , -- | end of the service definition
476 epilogue :: Text
477 }
478```
480# Custom function name builder
482Servant comes with three name builders included:
484- camelCase (the default)
485- concatCase
486- snakeCase
488Keeping the JQuery as an example, let's see the impact:
490``` haskell
491apiJS6 :: Text
492apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase }
493```
495This `Text` contains 2 Javascript functions:
497``` javascript
500var get_point = function(onSuccess, onError)
502 $.ajax(
503 { url: '/point'
504 , success: onSuccess
505 , error: onError
506 , type: 'GET'
507 });
510var get_books = function(q, onSuccess, onError)
512 $.ajax(
513 { url: '/books' + '?q=' + encodeURIComponent(q)
514 , success: onSuccess
515 , error: onError
516 , type: 'GET'
517 });
520```