Skip to content

Commit b97a352

Browse files
committed
tutorial: updated Javascript.lhs (and wrote some tests for it)
1 parent 0985e51 commit b97a352

File tree

7 files changed

+206
-40
lines changed

7 files changed

+206
-40
lines changed

doc/tutorial/.ghci

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
# Generating Javascript functions to query an API
22

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
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*.
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.
75

86
For this, we will consider a simple page divided in two parts. At the top, we
97
will have a search box that lets us search in a list of Haskell books by
@@ -32,10 +30,11 @@ import Data.Aeson
3230
import Data.Proxy
3331
import Data.Text as T (Text)
3432
import Data.Text.IO as T (writeFile, readFile)
35-
import qualified Data.Text as T
3633
import GHC.Generics
3734
import Language.Javascript.JQuery
3835
import Network.Wai
36+
import Network.Wai.Handler.Warp
37+
import qualified Data.Text as T
3938
import Servant
4039
import Servant.JS
4140
import System.Random
@@ -78,7 +77,8 @@ book :: Text -> Text -> Int -> Book
7877
book = Book
7978
```
8079
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.
8282

8383
``` haskell
8484
books :: [Book]
@@ -92,7 +92,10 @@ books =
9292
]
9393
```
9494
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.
9699
97100
``` haskell
98101
searchBook :: Monad m => Maybe Text -> m (Search Book)
@@ -106,7 +109,9 @@ searchBook (Just q) = return (mkSearch q books')
106109
q' = T.toLower q
107110
```
108111
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`.
110115
111116
``` haskell
112117
randomPoint :: MonadIO m => m Point
@@ -131,54 +136,93 @@ server = randomPoint
131136
132137
server' :: Server API'
133138
server' = server
134-
:<|> serveDirectory "tutorial/t9"
139+
:<|> serveDirectory "static"
135140
136141
app :: Application
137142
app = serve api' server'
143+
144+
main :: IO ()
145+
main = run 8000 app
138146
```
139147
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.
148+
Why two different API types, proxies and servers though? Simply because we
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.
141151
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`.
143155
144156
``` haskell
145157
apiJS :: Text
146158
apiJS = jsForAPI api vanillaJS
147159
```
148160
149-
This `String` contains 2 Javascript functions:
161+
This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
150162
151163
``` javascript
152-
153-
function getpoint(onSuccess, onError)
164+
var getPoint = function(onSuccess, onError)
154165
{
155-
$.ajax(
156-
{ url: '/point'
157-
, success: onSuccess
158-
, error: onError
159-
, method: 'GET'
160-
});
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);
161183
}
162184
163-
function getbooks(q, onSuccess, onError)
185+
var getBooks = function(q, onSuccess, onError)
164186
{
165-
$.ajax(
166-
{ url: '/books' + '?q=' + encodeURIComponent(q)
167-
, success: onSuccess
168-
, error: onError
169-
, method: 'GET'
170-
});
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);
171204
}
172205
```
173206
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:
175212
176213
``` haskell
177214
writeJSFiles :: IO ()
178215
writeJSFiles = do
179-
T.writeFile "getting-started/gs9/api.js" apiJS
216+
T.writeFile "static/api.js" apiJS
180217
jq <- T.readFile =<< Language.Javascript.JQuery.file
181-
T.writeFile "getting-started/gs9/jq.js" jq
218+
T.writeFile "static/jq.js" jq
182219
```
183220
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.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.hs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

doc/tutorial/tutorial.cabal

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
name: tutorial
22
version: 0.5
33
synopsis: The servant tutorial
4-
-- description:
54
homepage: http://haskell-servant.github.io/
65
license: BSD3
76
license-file: LICENSE
87
author: Servant Contributors
98
maintainer: [email protected]
10-
-- copyright:
11-
-- category:
129
build-type: Simple
13-
-- extra-source-files:
1410
cabal-version: >=1.10
1511

1612
library
@@ -19,13 +15,11 @@ library
1915
, Docs
2016
, Javascript
2117
, Server
22-
-- other-modules:
23-
-- other-extensions:
2418
build-depends: base == 4.*
2519
, base-compat
2620
, text
2721
, aeson
28-
, aeson-compat
22+
, aeson-compat
2923
, blaze-html
3024
, directory
3125
, blaze-markup
@@ -49,9 +43,18 @@ library
4943
, transformers
5044
, markdown-unlit >= 0.4
5145
, http-client
52-
-- hs-source-dirs:
5346
default-language: Haskell2010
5447
ghc-options: -Wall -Werror -pgmL markdown-unlit
5548
-- to silence aeson-0.10 warnings:
5649
ghc-options: -fno-warn-missing-methods
5750
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.*

0 commit comments

Comments
 (0)