git.iko.soy/typeable/servant
on v0.7 help sign in
typeable/servant/doc/tutorial/Javascript.lhs
228 lines 6.9 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 :<|> serveDirectory "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.
152Very similarly to how one can derive haskell functions, we can derive the
153javascript with just a simple function call to `jsForAPI` from
154`Servant.JQuery`.
156``` haskell
157apiJS :: Text
158apiJS = jsForAPI api vanillaJS
159```
161This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
163``` javascript
164var getPoint = function(onSuccess, onError)
166 var xhr = new XMLHttpRequest();
167 xhr.open('GET', '/point', true);
168 xhr.setRequestHeader("Accept","application/json");
169 xhr.onreadystatechange = function (e) {
170 if (xhr.readyState == 4) {
171 if (xhr.status == 204 || xhr.status == 205) {
172 onSuccess();
173 } else if (xhr.status >= 200 && xhr.status < 300) {
174 var value = JSON.parse(xhr.responseText);
175 onSuccess(value);
176 } else {
177 var value = JSON.parse(xhr.responseText);
178 onError(value);
179 }
180 }
181 }
182 xhr.send(null);
185var getBooks = function(q, onSuccess, onError)
187 var xhr = new XMLHttpRequest();
188 xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
189 xhr.setRequestHeader("Accept","application/json");
190 xhr.onreadystatechange = function (e) {
191 if (xhr.readyState == 4) {
192 if (xhr.status == 204 || xhr.status == 205) {
193 onSuccess();
194 } else if (xhr.status >= 200 && xhr.status < 300) {
195 var value = JSON.parse(xhr.responseText);
196 onSuccess(value);
197 } else {
198 var value = JSON.parse(xhr.responseText);
199 onError(value);
200 }
201 }
202 }
203 xhr.send(null);
205```
207We created a directory `static` that contains two static files: `index.html`,
208which is the entrypoint to our little web application; and `ui.js`, which
209contains some hand-written javascript. This javascript code assumes the two
210generated functions `getPoint` and `getBooks` in scope. Therefore we need to
211write the generated javascript into a file:
213``` haskell
214writeJSFiles :: IO ()
215writeJSFiles = do
216 T.writeFile "static/api.js" apiJS
217 jq <- T.readFile =<< Language.Javascript.JQuery.file
218 T.writeFile "static/jq.js" jq
219```
221(We're also writing the jquery library into a file, as it's also used by
222`ui.js`.) `static/api.js` will be included in `index.html` and the two
223generated functions will therefore be available in `ui.js`.
225And we're good to go. You can start the `main` function of this file and go to
226`http://localhost:8000/`. Start typing in the name of one of the authors in our
227database or part of a book title, and check out how long it takes to
228approximate pi using the method mentioned above.