From 4f548f5025396ffb6cbb4c40dfbe13520cf83bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sat, 12 Mar 2016 16:03:57 +0800 Subject: [PATCH] tutorial: updated Javascript.lhs (and wrote some tests for it) --- doc/tutorial/.ghci | 2 +- doc/tutorial/Javascript.lhs | 106 ++++++++++++++++++++-------- doc/tutorial/static/index.html | 26 +++++++ doc/tutorial/static/ui.js | 60 ++++++++++++++++ doc/tutorial/test/JavascriptSpec.hs | 32 +++++++++ doc/tutorial/test/Spec.hs | 1 + doc/tutorial/tutorial.cabal | 19 ++--- 7 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 doc/tutorial/static/index.html create mode 100644 doc/tutorial/static/ui.js create mode 100644 doc/tutorial/test/JavascriptSpec.hs create mode 100644 doc/tutorial/test/Spec.hs diff --git a/doc/tutorial/.ghci b/doc/tutorial/.ghci index 7d8e760c..d8e88521 100644 --- a/doc/tutorial/.ghci +++ b/doc/tutorial/.ghci @@ -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 diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 600c8327..4054e4b3 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -1,9 +1,7 @@ # Generating Javascript functions to query an API -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 -use *jQuery* but you could very easily adapt the code to generate ajax requests -based on vanilla javascript or another library than *jQuery*. +We will now see how **servant** lets you turn an API type into javascript +functions that you can call to query a webservice. 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 @@ -32,10 +30,11 @@ import Data.Aeson import Data.Proxy import Data.Text as T (Text) import Data.Text.IO as T (writeFile, readFile) -import qualified Data.Text as T import GHC.Generics import Language.Javascript.JQuery import Network.Wai +import Network.Wai.Handler.Warp +import qualified Data.Text as T import Servant import Servant.JS import System.Random @@ -78,7 +77,8 @@ book :: Text -> Text -> Int -> 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 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 searchBook :: Monad m => Maybe Text -> m (Search Book) @@ -106,7 +109,9 @@ searchBook (Just q) = return (mkSearch q books') 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 randomPoint :: MonadIO m => m Point @@ -131,54 +136,93 @@ server = randomPoint server' :: Server API' server' = server - :<|> serveDirectory "tutorial/t9" + :<|> serveDirectory "static" app :: Application 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 apiJS :: Text apiJS = jsForAPI api vanillaJS ``` -This `String` contains 2 Javascript functions: +This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks': ``` javascript - -function getpoint(onSuccess, onError) +var getPoint = function(onSuccess, onError) { - $.ajax( - { url: '/point' - , success: onSuccess - , error: onError - , method: 'GET' - }); + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/point', true); + xhr.setRequestHeader("Accept","application/json"); + xhr.onreadystatechange = function (e) { + 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( - { url: '/books' + '?q=' + encodeURIComponent(q) - , success: onSuccess - , error: onError - , method: 'GET' - }); + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true); + xhr.setRequestHeader("Accept","application/json"); + xhr.onreadystatechange = function (e) { + 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 writeJSFiles :: IO () writeJSFiles = do - T.writeFile "getting-started/gs9/api.js" apiJS + T.writeFile "static/api.js" apiJS 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. diff --git a/doc/tutorial/static/index.html b/doc/tutorial/static/index.html new file mode 100644 index 00000000..6a047c1c --- /dev/null +++ b/doc/tutorial/static/index.html @@ -0,0 +1,26 @@ + + + + + + Tutorial - 9 - servant-jquery + + +

Books

+ +
+

Results for ""

+ +
+
+

Approximating π

+

Count: 0

+

Successes: 0

+

+ + + + + + diff --git a/doc/tutorial/static/ui.js b/doc/tutorial/static/ui.js new file mode 100644 index 00000000..8bcae8d8 --- /dev/null +++ b/doc/tutorial/static/ui.js @@ -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 = '
  • ' + book.title + ', ' + + book.author + ' - ' + book.year + '
  • '; + 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); diff --git a/doc/tutorial/test/JavascriptSpec.hs b/doc/tutorial/test/JavascriptSpec.hs new file mode 100644 index 00000000..2d6007a5 --- /dev/null +++ b/doc/tutorial/test/JavascriptSpec.hs @@ -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 diff --git a/doc/tutorial/test/Spec.hs b/doc/tutorial/test/Spec.hs new file mode 100644 index 00000000..a824f8c3 --- /dev/null +++ b/doc/tutorial/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 189140de..9664ce45 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -1,16 +1,12 @@ name: tutorial version: 0.5 synopsis: The servant tutorial --- description: homepage: http://haskell-servant.github.io/ license: BSD3 license-file: LICENSE author: Servant Contributors maintainer: haskell-servant-maintainers@googlegroups.com --- copyright: --- category: build-type: Simple --- extra-source-files: cabal-version: >=1.10 library @@ -19,13 +15,11 @@ library , Docs , Javascript , Server - -- other-modules: - -- other-extensions: build-depends: base == 4.* , base-compat , text , aeson - , aeson-compat + , aeson-compat , blaze-html , directory , blaze-markup @@ -49,9 +43,18 @@ library , transformers , markdown-unlit >= 0.4 , http-client - -- hs-source-dirs: default-language: Haskell2010 ghc-options: -Wall -Werror -pgmL markdown-unlit -- to silence aeson-0.10 warnings: ghc-options: -fno-warn-missing-methods 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.*