tutorial: updated Javascript.lhs (and wrote some tests for it)
This commit is contained in:
parent
0985e51022
commit
b97a352773
7 changed files with 206 additions and 40 deletions
|
@ -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
|
||||||
|
|
|
@ -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 π 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.
|
||||||
|
|
26
doc/tutorial/static/index.html
Normal file
26
doc/tutorial/static/index.html
Normal 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 π</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
60
doc/tutorial/static/ui.js
Normal 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);
|
32
doc/tutorial/test/JavascriptSpec.hs
Normal file
32
doc/tutorial/test/JavascriptSpec.hs
Normal 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
|
1
doc/tutorial/test/Spec.hs
Normal file
1
doc/tutorial/test/Spec.hs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
@ -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.*
|
||||||
|
|
Loading…
Reference in a new issue