tutorial: updated Javascript.lhs (and wrote some tests for it)

This commit is contained in:
Sönke Hahn 2016-03-12 16:03:57 +08:00
parent 0985e51022
commit b97a352773
7 changed files with 206 additions and 40 deletions

View file

@ -1 +1 @@
:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing :set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing -itest

View file

@ -1,9 +1,7 @@
# Generating Javascript functions to query an API # Generating Javascript functions to query an API
We will now see how *servant* lets you turn an API type into javascript We will now see how **servant** lets you turn an API type into javascript
functions that you can call to query a webservice. The derived code assumes you functions that you can call to query a webservice.
use *jQuery* but you could very easily adapt the code to generate ajax requests
based on vanilla javascript or another library than *jQuery*.
For this, we will consider a simple page divided in two parts. At the top, we For this, we will consider a simple page divided in two parts. At the top, we
will have a search box that lets us search in a list of Haskell books by will have a search box that lets us search in a list of Haskell books by
@ -32,10 +30,11 @@ import Data.Aeson
import Data.Proxy import Data.Proxy
import Data.Text as T (Text) import Data.Text as T (Text)
import Data.Text.IO as T (writeFile, readFile) import Data.Text.IO as T (writeFile, readFile)
import qualified Data.Text as T
import GHC.Generics import GHC.Generics
import Language.Javascript.JQuery import Language.Javascript.JQuery
import Network.Wai import Network.Wai
import Network.Wai.Handler.Warp
import qualified Data.Text as T
import Servant import Servant
import Servant.JS import Servant.JS
import System.Random import System.Random
@ -78,7 +77,8 @@ book :: Text -> Text -> Int -> Book
book = Book book = Book
``` ```
We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books. We need a "book database". For the purpose of this guide, let's restrict
ourselves to the following books.
``` haskell ``` haskell
books :: [Book] books :: [Book]
@ -92,7 +92,10 @@ books =
] ]
``` ```
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. 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.
``` haskell ``` haskell
searchBook :: Monad m => Maybe Text -> m (Search Book) searchBook :: Monad m => Maybe Text -> m (Search Book)
@ -106,7 +109,9 @@ searchBook (Just q) = return (mkSearch q books')
q' = T.toLower q q' = T.toLower q
``` ```
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`. 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`.
``` haskell ``` haskell
randomPoint :: MonadIO m => m Point randomPoint :: MonadIO m => m Point
@ -131,54 +136,93 @@ server = randomPoint
server' :: Server API' server' :: Server API'
server' = server server' = server
:<|> serveDirectory "tutorial/t9" :<|> serveDirectory "static"
app :: Application app :: Application
app = serve api' server' app = serve api' server'
main :: IO ()
main = run 8000 app
``` ```
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. 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.
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`. 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`.
``` haskell ``` haskell
apiJS :: Text apiJS :: Text
apiJS = jsForAPI api vanillaJS apiJS = jsForAPI api vanillaJS
``` ```
This `String` contains 2 Javascript functions: This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
``` javascript ``` javascript
var getPoint = function(onSuccess, onError)
function getpoint(onSuccess, onError)
{ {
$.ajax( var xhr = new XMLHttpRequest();
{ url: '/point' xhr.open('GET', '/point', true);
, success: onSuccess xhr.setRequestHeader("Accept","application/json");
, error: onError xhr.onreadystatechange = function (e) {
, method: 'GET' if (xhr.readyState == 4) {
}); if (xhr.status == 204 || xhr.status == 205) {
onSuccess();
} else if (xhr.status >= 200 && xhr.status < 300) {
var value = JSON.parse(xhr.responseText);
onSuccess(value);
} else {
var value = JSON.parse(xhr.responseText);
onError(value);
}
}
}
xhr.send(null);
} }
function getbooks(q, onSuccess, onError) var getBooks = function(q, onSuccess, onError)
{ {
$.ajax( var xhr = new XMLHttpRequest();
{ url: '/books' + '?q=' + encodeURIComponent(q) xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
, success: onSuccess xhr.setRequestHeader("Accept","application/json");
, error: onError xhr.onreadystatechange = function (e) {
, method: 'GET' if (xhr.readyState == 4) {
}); if (xhr.status == 204 || xhr.status == 205) {
onSuccess();
} else if (xhr.status >= 200 && xhr.status < 300) {
var value = JSON.parse(xhr.responseText);
onSuccess(value);
} else {
var value = JSON.parse(xhr.responseText);
onError(value);
}
}
}
xhr.send(null);
} }
``` ```
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. We created a directory `static` that contains two static files: `index.html`,
which is the entrypoint to our little web application; and `ui.js`, which
contains some hand-written javascript. This javascript code assumes the two
generated functions `getPoint` and `getBooks` in scope. Therefore we need to
write the generated javascript into a file:
``` haskell ``` haskell
writeJSFiles :: IO () writeJSFiles :: IO ()
writeJSFiles = do writeJSFiles = do
T.writeFile "getting-started/gs9/api.js" apiJS T.writeFile "static/api.js" apiJS
jq <- T.readFile =<< Language.Javascript.JQuery.file jq <- T.readFile =<< Language.Javascript.JQuery.file
T.writeFile "getting-started/gs9/jq.js" jq T.writeFile "static/jq.js" jq
``` ```
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. (We're also writing the jquery library into a file, as it's also used by
`ui.js`.) `static/api.js` will be included in `index.html` and the two
generated functions will therefore be available in `ui.js`.
And we're good to go. You can start the `main` function of this file and go to
`http://localhost:8000/`. 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.

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Tutorial - 9 - servant-jquery</title>
</head>
<body>
<h1>Books</h1>
<input type="search" name="q" id="q" placeholder="Search author or book title..." autocomplete="off"/>
<div>
<p>Results for <strong id="query">""</strong></p>
<ul id="results">
</ul>
</div>
<hr />
<h1>Approximating &pi;</h1>
<p>Count: <span id="count">0</span></p>
<p>Successes: <span id="successes">0</span></p>
<p id="pi"></p>
<script type="text/javascript" src="/jq.js"></script>
<script type="text/javascript" src="/api.js"></script>
<script type="text/javascript" src="/ui.js"></script>
</body>

60
doc/tutorial/static/ui.js Normal file
View file

@ -0,0 +1,60 @@
/* book search */
function updateResults(data)
{
console.log(data);
$('#results').html("");
$('#query').text("\"" + data.query + "\"");
for(var i = 0; i < data.results.length; i++)
{
$('#results').append(renderBook(data.results[i]));
}
}
function renderBook(book)
{
var li = '<li><strong>' + book.title + '</strong>, <i>'
+ book.author + '</i> - ' + book.year + '</li>';
return li;
}
function searchBooks()
{
var q = $('#q').val();
getBooks(q, updateResults, console.log)
}
searchBooks();
$('#q').keyup(function() {
searchBooks();
});
/* approximating pi */
var count = 0;
var successes = 0;
function f(data)
{
var x = data.x, y = data.y;
if(x*x + y*y <= 1)
{
successes++;
}
count++;
update('#count', count);
update('#successes', successes);
update('#pi', 4*successes/count);
}
function update(id, val)
{
$(id).text(val);
}
function refresh()
{
getPoint(f, console.log);
}
window.setInterval(refresh, 200);

View file

@ -0,0 +1,32 @@
{-# LANGUAGE OverloadedStrings #-}
module JavascriptSpec where
import Data.List
import Data.String
import Data.String.Conversions
import Test.Hspec
import Test.Hspec.Wai
import Javascript
spec :: Spec
spec = do
describe "apiJS" $ do
it "is contained verbatim in Javascript.lhs" $ do
code <- readFile "Javascript.lhs"
cs apiJS `shouldSatisfy` (`isInfixOf` code)
describe "writeJSFiles" $ do
it "[not a test] write apiJS to static/api.js" $ do
writeJSFiles
describe "app" $ with (return app) $ do
context "/api.js" $ do
it "delivers apiJS" $ do
get "/api.js" `shouldRespondWith` (fromString (cs apiJS))
context "/" $ do
it "delivers something" $ do
get "" `shouldRespondWith` 200
get "/" `shouldRespondWith` 200

View file

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

View file

@ -1,16 +1,12 @@
name: tutorial name: tutorial
version: 0.5 version: 0.5
synopsis: The servant tutorial synopsis: The servant tutorial
-- description:
homepage: http://haskell-servant.github.io/ homepage: http://haskell-servant.github.io/
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Servant Contributors author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com maintainer: haskell-servant-maintainers@googlegroups.com
-- copyright:
-- category:
build-type: Simple build-type: Simple
-- extra-source-files:
cabal-version: >=1.10 cabal-version: >=1.10
library library
@ -19,8 +15,6 @@ library
, Docs , Docs
, Javascript , Javascript
, Server , Server
-- other-modules:
-- other-extensions:
build-depends: base == 4.* build-depends: base == 4.*
, base-compat , base-compat
, text , text
@ -49,9 +43,18 @@ library
, transformers , transformers
, markdown-unlit >= 0.4 , markdown-unlit >= 0.4
, http-client , http-client
-- hs-source-dirs:
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall -Werror -pgmL markdown-unlit ghc-options: -Wall -Werror -pgmL markdown-unlit
-- to silence aeson-0.10 warnings: -- to silence aeson-0.10 warnings:
ghc-options: -fno-warn-missing-methods ghc-options: -fno-warn-missing-methods
ghc-options: -fno-warn-name-shadowing ghc-options: -fno-warn-name-shadowing
test-suite spec
type: exitcode-stdio-1.0
ghc-options:
-Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
default-language: Haskell2010
hs-source-dirs: test
main-is: Spec.hs
build-depends:
base == 4.*