on master help sign in
tutorial: updated Javascript.lhs (and wrote some tests for it)
b97a352Sönke Hahn
doc/tutorial/.ghci+1−1
@@ -1 +1 @@
1−:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing
1+:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing -itest
doc/tutorial/Javascript.lhs+75−31
@@ -1,9 +1,7 @@
1# Generating Javascript functions to query an API
1# Generating Javascript functions to query an API
2
2
3−We will now see how *servant* lets you turn an API type into javascript
3+We will now see how **servant** lets you turn an API type into javascript
4−functions that you can call to query a webservice. The derived code assumes you
4+functions that you can call to query a webservice.
5−use *jQuery* but you could very easily adapt the code to generate ajax requests
6−based on vanilla javascript or another library than *jQuery*.
7
5
8For this, we will consider a simple page divided in two parts. At the top, we
6For this, we will consider a simple page divided in two parts. At the top, we
9will have a search box that lets us search in a list of Haskell books by
7will have a search box that lets us search in a list of Haskell books by
@@ -32,10 +30,11 @@ import Data.Aeson
32import Data.Proxy
30import Data.Proxy
33import Data.Text as T (Text)
31import Data.Text as T (Text)
34import Data.Text.IO as T (writeFile, readFile)
32import Data.Text.IO as T (writeFile, readFile)
35−import qualified Data.Text as T
36import GHC.Generics
33import GHC.Generics
37import Language.Javascript.JQuery
34import Language.Javascript.JQuery
38import Network.Wai
35import Network.Wai
36+import Network.Wai.Handler.Warp
37+import qualified Data.Text as T
39import Servant
38import Servant
40import Servant.JS
39import Servant.JS
41import System.Random
40import System.Random
@@ -78,7 +77,8 @@ book :: Text -> Text -> Int -> Book
78book = Book
77book = Book
79```
78```
80
79
81−We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books.
80+We need a "book database". For the purpose of this guide, let's restrict
81+ourselves to the following books.
82
82
83``` haskell
83``` haskell
84books :: [Book]
84books :: [Book]
@@ -92,7 +92,10 @@ books =
92 ]
92 ]
93```
93```
94
94
95−Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is.
95+Now, given an optional search string `q`, we want to perform a case insensitive
96+search in that list of books. We're obviously not going to try and implement
97+the best possible algorithm, this is out of scope for this tutorial. The
98+following simple linear scan will do, given how small our list is.
96
99
97``` haskell
100``` haskell
98searchBook :: Monad m => Maybe Text -> m (Search Book)
101searchBook :: Monad m => Maybe Text -> m (Search Book)
@@ -106,7 +109,9 @@ searchBook (Just q) = return (mkSearch q books')
106 q' = T.toLower q
109 q' = T.toLower q
107```
110```
108
111
109−We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`.
112+We 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`.
110
115
111``` haskell
116``` haskell
112randomPoint :: MonadIO m => m Point
117randomPoint :: MonadIO m => m Point
@@ -131,54 +136,93 @@ server = randomPoint
131
136
132server' :: Server API'
137server' :: Server API'
133server' = server
138server' = server
134− :<|> serveDirectory "tutorial/t9"
139+ :<|> serveDirectory "static"
135
140
136app :: Application
141app :: Application
137app = serve api' server'
142app = serve api' server'
143+
144+main :: IO ()
145+main = run 8000 app
138```
146```
139
147
148+Why two different API types, proxies and servers though? Simply because we
140−Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
149+don't want to generate javascript functions for the `Raw` part of our API type,
150+so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
141
151
142−Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`.
152+Very similarly to how one can derive haskell functions, we can derive the
153+javascript with just a simple function call to `jsForAPI` from
154+`Servant.JQuery`.
143
155
144``` haskell
156``` haskell
145apiJS :: Text
157apiJS :: Text
146apiJS = jsForAPI api vanillaJS
158apiJS = jsForAPI api vanillaJS
147```
159```
148
160
149−This `String` contains 2 Javascript functions:
161+This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
150
162
151``` javascript
163``` javascript
152−
153−function getpoint(onSuccess, onError)
164+var getPoint = function(onSuccess, onError)
154{
165{
155− $.ajax(
166+ var xhr = new XMLHttpRequest();
156− { url: '/point'
167+ xhr.open('GET', '/point', true);
157− , success: onSuccess
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);
158− , error: onError
178+ onError(value);
159− , method: 'GET'
179+ }
160− });
180+ }
181+ }
182+ xhr.send(null);
161}
183}
162
184
163−function getbooks(q, onSuccess, onError)
185+var getBooks = function(q, onSuccess, onError)
164{
186{
165− $.ajax(
187+ var xhr = new XMLHttpRequest();
166− { url: '/books' + '?q=' + encodeURIComponent(q)
188+ xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
167− , success: onSuccess
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);
168− , error: onError
199+ onError(value);
169− , method: 'GET'
200+ }
170− });
201+ }
202+ }
203+ xhr.send(null);
171}
204}
172```
205```
173
206
174−Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package.
207+We created a directory `static` that contains two static files: `index.html`,
208+which is the entrypoint to our little web application; and `ui.js`, which
209+contains some hand-written javascript. This javascript code assumes the two
210+generated functions `getPoint` and `getBooks` in scope. Therefore we need to
211+write the generated javascript into a file:
175
212
176``` haskell
213``` haskell
177writeJSFiles :: IO ()
214writeJSFiles :: IO ()
178writeJSFiles = do
215writeJSFiles = do
179− T.writeFile "getting-started/gs9/api.js" apiJS
216+ T.writeFile "static/api.js" apiJS
180 jq <- T.readFile =<< Language.Javascript.JQuery.file
217 jq <- T.readFile =<< Language.Javascript.JQuery.file
181− T.writeFile "getting-started/gs9/jq.js" jq
218+ T.writeFile "static/jq.js" jq
182```
219```
183
220
184−And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate &pi; using the method mentioned above.
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
223+generated functions will therefore be available in `ui.js`.
224+
225+And 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
227+database or part of a book title, and check out how long it takes to
228+approximate pi using the method mentioned above.
doc/tutorial/static/index.htmlnew file+26
@@ -0,0 +1,26 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ <head>
4+ <meta name="viewport" content="width=device-width, initial-scale=1">
5+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6+ <title>Tutorial - 9 - servant-jquery</title>
7+ </head>
8+ <body>
9+ <h1>Books</h1>
10+ <input type="search" name="q" id="q" placeholder="Search author or book title..." autocomplete="off"/>
11+ <div>
12+ <p>Results for <strong id="query">""</strong></p>
13+ <ul id="results">
14+ </ul>
15+ </div>
16+ <hr />
17+ <h1>Approximating &pi;</h1>
18+ <p>Count: <span id="count">0</span></p>
19+ <p>Successes: <span id="successes">0</span></p>
20+ <p id="pi"></p>
21+
22+ <script type="text/javascript" src="/jq.js"></script>
23+ <script type="text/javascript" src="/api.js"></script>
24+ <script type="text/javascript" src="/ui.js"></script>
25+
26+ </body>
doc/tutorial/static/ui.jsnew file+60
@@ -0,0 +1,60 @@
1+/* book search */
2+function updateResults(data)
3+{
4+ console.log(data);
5+ $('#results').html("");
6+ $('#query').text("\"" + data.query + "\"");
7+ for(var i = 0; i < data.results.length; i++)
8+ {
9+ $('#results').append(renderBook(data.results[i]));
10+ }
11+}
12+
13+function renderBook(book)
14+{
15+ var li = '<li><strong>' + book.title + '</strong>, <i>'
16+ + book.author + '</i> - ' + book.year + '</li>';
17+ return li;
18+}
19+
20+function searchBooks()
21+{
22+ var q = $('#q').val();
23+ getBooks(q, updateResults, console.log)
24+}
25+
26+searchBooks();
27+$('#q').keyup(function() {
28+ searchBooks();
29+});
30+
31+/* approximating pi */
32+var count = 0;
33+var successes = 0;
34+
35+function f(data)
36+{
37+ var x = data.x, y = data.y;
38+ if(x*x + y*y <= 1)
39+ {
40+ successes++;
41+ }
42+
43+ count++;
44+
45+ update('#count', count);
46+ update('#successes', successes);
47+ update('#pi', 4*successes/count);
48+}
49+
50+function update(id, val)
51+{
52+ $(id).text(val);
53+}
54+
55+function refresh()
56+{
57+ getPoint(f, console.log);
58+}
59+
60+window.setInterval(refresh, 200);
doc/tutorial/test/JavascriptSpec.hsnew file+32
@@ -0,0 +1,32 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module JavascriptSpec where
4+
5+import Data.List
6+import Data.String
7+import Data.String.Conversions
8+import Test.Hspec
9+import Test.Hspec.Wai
10+
11+import Javascript
12+
13+spec :: Spec
14+spec = do
15+ describe "apiJS" $ do
16+ it "is contained verbatim in Javascript.lhs" $ do
17+ code <- readFile "Javascript.lhs"
18+ cs apiJS `shouldSatisfy` (`isInfixOf` code)
19+
20+ describe "writeJSFiles" $ do
21+ it "[not a test] write apiJS to static/api.js" $ do
22+ writeJSFiles
23+
24+ describe "app" $ with (return app) $ do
25+ context "/api.js" $ do
26+ it "delivers apiJS" $ do
27+ get "/api.js" `shouldRespondWith` (fromString (cs apiJS))
28+
29+ context "/" $ do
30+ it "delivers something" $ do
31+ get "" `shouldRespondWith` 200
32+ get "/" `shouldRespondWith` 200
doc/tutorial/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
doc/tutorial/tutorial.cabal+11−8
@@ -1,16 +1,12 @@
1name: tutorial
1name: tutorial
2version: 0.5
2version: 0.5
3synopsis: The servant tutorial
3synopsis: The servant tutorial
4−-- description:
5homepage: http://haskell-servant.github.io/
4homepage: http://haskell-servant.github.io/
6license: BSD3
5license: BSD3
7license-file: LICENSE
6license-file: LICENSE
8author: Servant Contributors
7author: Servant Contributors
9maintainer: haskell-servant-maintainers@googlegroups.com
8maintainer: haskell-servant-maintainers@googlegroups.com
10−-- copyright:
11−-- category:
12build-type: Simple
9build-type: Simple
13−-- extra-source-files:
14cabal-version: >=1.10
10cabal-version: >=1.10
15
11
16library
12library
@@ -19,13 +15,11 @@ library
19 , Docs
15 , Docs
20 , Javascript
16 , Javascript
21 , Server
17 , Server
22− -- other-modules:
23− -- other-extensions:
24 build-depends: base == 4.*
18 build-depends: base == 4.*
25 , base-compat
19 , base-compat
26 , text
20 , text
27 , aeson
21 , aeson
28− , aeson-compat
22+ , aeson-compat
29 , blaze-html
23 , blaze-html
30 , directory
24 , directory
31 , blaze-markup
25 , blaze-markup
@@ -49,9 +43,18 @@ library
49 , transformers
43 , transformers
50 , markdown-unlit >= 0.4
44 , markdown-unlit >= 0.4
51 , http-client
45 , http-client
52− -- hs-source-dirs:
53 default-language: Haskell2010
46 default-language: Haskell2010
54 ghc-options: -Wall -Werror -pgmL markdown-unlit
47 ghc-options: -Wall -Werror -pgmL markdown-unlit
55 -- to silence aeson-0.10 warnings:
48 -- to silence aeson-0.10 warnings:
56 ghc-options: -fno-warn-missing-methods
49 ghc-options: -fno-warn-missing-methods
57 ghc-options: -fno-warn-name-shadowing
50 ghc-options: -fno-warn-name-shadowing
51+
52+test-suite spec
53+ type: exitcode-stdio-1.0
54+ ghc-options:
55+ -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
56+ default-language: Haskell2010
57+ hs-source-dirs: test
58+ main-is: Spec.hs
59+ build-depends:
60+ base == 4.*