From 95d077f5867fe70f253a54640ef03ab8a6c131dd Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Tue, 10 May 2016 17:44:01 +0200 Subject: [PATCH 1/4] Add more JS documentation. Contributed by freezeboy in PR #11 - https://github.com/haskell-servant/haskell-servant.github.io/pull/11/files --- doc/tutorial/Javascript.lhs | 355 +++++++++++++++++++++++++++++++----- 1 file changed, 313 insertions(+), 42 deletions(-) diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 4054e4b3..921fe40f 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -151,57 +151,36 @@ 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`. +`Servant.JS`. ``` haskell apiJS :: Text -apiJS = jsForAPI api vanillaJS +apiJS = jsForAPI api jquery ``` This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks': ``` javascript -var getPoint = function(onSuccess, onError) -{ - 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); -} +var getPoint = function (onSuccess, onError) + { + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , method: 'GET' + }); +}; -var getBooks = function(q, onSuccess, onError) -{ - 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); -} + +var getBooks = function (q, onSuccess, onError) + { + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , method: 'GET' + }); +}; ``` We created a directory `static` that contains two static files: `index.html`, @@ -226,3 +205,295 @@ 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. + +## Customizations + +Instead of calling `jquery`, you can call its variant `jqueryWith`. +Here are the type definitions + +```haskell ignore +jquery :: JavaScriptGenerator +jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator +``` + +The `CommonGeneratorOptions` will let you define different behaviors to +change how functions are generated. Here is the definition of currently +available options: + +```haskell +data CommonGeneratorOptions = CommonGeneratorOptions + { + -- | function generating function names + functionNameBuilder :: FunctionName -> String + -- | name used when a user want to send the request body (to let you redefine it) + , requestBody :: String + -- | name of the callback parameter when the request was successful + , successCallback :: String + -- | name of the callback parameter when the request reported an error + , errorCallback :: String + -- | namespace on which we define the js function (empty mean local var) + , moduleName :: String + -- | a prefix that should be prepended to the URL in the generated JS + , urlPrefix :: String + } +``` + +This pattern is available with all supported backends, and default values are provided. + +## Vanilla support + +If you don't use JQuery for your application, you can reduce your +dependencies to simply use the `XMLHttpRequest` object from the standard API. + +Use the same code as before but simply replace the previous `apiJS` with +the following one: + +``` haskell +apiJS :: String +apiJS = jsForAPI api vanillaJS +``` + +The rest is *completely* unchanged. + +The output file is a bit different, but it has the same parameters, + +``` javascript + +var getPoint = function (onSuccess, onError) +{ + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/point', true); + + xhr.onreadystatechange = function (e) { + if (xhr.readyState == 4) { + var value = JSON.parse(xhr.responseText); + if (xhr.status == 200 || xhr.status == 201) { + onSuccess(value); + } else { + onError(value); + } + } + } + xhr.send(null); +}; + +var getBooks = function (q, onSuccess, onError) +{ + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true); + + xhr.onreadystatechange = function (e) { + if (xhr.readyState == 4) { + var value = JSON.parse(xhr.responseText); + if (xhr.status == 200 || xhr.status == 201) { + onSuccess(value); + } else { + onError(value); + } + } + } + xhr.send(null); +}; +``` + +And that's all, your web service can of course be accessible from those +two clients at the same time! + +## Axios support + +### Simple usage + +If you use Axios library for your application, we support that too! + +Use the same code as before but simply replace the previous `apiJS` with +the following one: + +``` haskell +apiJS :: String +apiJS = jsForAPI api $ axios defAxiosOptions +``` + +The rest is *completely* unchanged. + +The output file is a bit different, + +``` javascript + +var getPoint = function () +{ + return axios({ url: '/point' + , method: 'get' + }); +}; + +var getBooks = function (q) +{ + return axios({ url: '/books' + '?q=' + encodeURIComponent(q) + , method: 'get' + }); +}; +``` + +**Caution:** In order to support the promise style of the API, there are no onSuccess +nor onError callback functions. + +### Defining Axios configuration + +Axios lets you define a 'configuration' to determine the behavior of the +program when the AJAX request is sent. + +We mapped this into a configuration + +``` haskell +data AxiosOptions = AxiosOptions + { -- | indicates whether or not cross-site Access-Control requests + -- should be made using credentials + withCredentials :: !Bool + -- | the name of the cookie to use as a value for xsrf token + , xsrfCookieName :: !(Maybe String) + -- | the name of the header to use as a value for xsrf token + , xsrfHeaderName :: !(Maybe String) + } +``` + +## Angular support + +### Simple usage + +You can apply the same procedure as with `vanillaJS` and `jquery`, and +generate top level functions. + +The difference is that `angular` Generator always takes an argument. + +``` haskell +apiJS :: String +apiJS = jsForAPI api $ angular defAngularOptions +``` + +The generated code will be a bit different than previous generators. An extra +argument `$http` will be added to let Angular magical Dependency Injector +operate. + +**Caution:** In order to support the promise style of the API, there are no onSuccess +nor onError callback functions. + +``` javascript + +var getPoint = function($http) +{ + return $http( + { url: '/counter' + , method: 'GET' + }); +} + +var getBooks = function($http, q) +{ + return $http( + { url: '/books' + '?q=' + encodeURIComponent(q), true); + , method: 'GET' + }); +} +``` + +You can then build your controllers easily + +``` javascript + +app.controller("MyController", function($http) { + this.getPoint = getPoint($http) + .success(/* Do something */) + .error(/* Report error */); + + this.getPoint = getBooks($http, q) + .success(/* Do something */) + .error(/* Report error */); +}); +``` + +### Service generator + +You can also generate automatically a service to wrap the whole API as +a single Angular service: + +``` javascript +app.service('MyService', function($http) { + return ({ + postCounter: function() + { + return $http( + { url: '/counter' + , method: 'POST' + }); + }, + getCounter: function() + { + return $http( + { url: '/books' + '?q=' + encodeURIComponent(q), true); + , method: 'GET' + }); + } + }); +}); +``` + +To do so, you just have to use an alternate generator. + +``` haskell +apiJS :: String +apiJS = jsForAPI api $ angularService defAngularOptions +``` + +Again, it is possible to customize some portions with the options. + +``` haskell +data AngularOptions = AngularOptions + { -- | When generating code with wrapInService, name of the service to generate, default is 'app' + serviceName :: String + , -- | beginning of the service definition + prologue :: String -> String -> String + , -- | end of the service definition + epilogue :: String + } +``` + +# Custom function name builder + +Servant comes with three name builders included: + +- camelCase (the default) +- concatCase +- snakeCase + +Keeping the JQuery as an example, let's see the impact: + +``` haskell +apiJS :: String +apiJS = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder: snakeCase } +``` + +This `String` contains 2 Javascript functions: + +``` javascript + +var getPoint = function (onSuccess, onError) +{ + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , method: 'GET' + }); +}; + +var getBooks = function (q, onSuccess, onError) +{ + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , method: 'GET' + }); +}; +``` + From 9e71fde7562a6c1b7edf689cb6cc8a7773e28b1e Mon Sep 17 00:00:00 2001 From: freezeboy Date: Tue, 10 May 2016 23:04:19 +0200 Subject: [PATCH 2/4] Fixing typo in Javascript.lhs --- doc/tutorial/Javascript.lhs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 921fe40f..09c21df3 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -469,7 +469,7 @@ Keeping the JQuery as an example, let's see the impact: ``` haskell apiJS :: String -apiJS = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder: snakeCase } +apiJS = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase } ``` This `String` contains 2 Javascript functions: From 7aa550aa051d5868a09a2561ae33727409b3146b Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Thu, 12 May 2016 13:20:02 +0200 Subject: [PATCH 3/4] Make Javascript tutorial compile. --- doc/tutorial/Javascript.lhs | 179 +++++++++++++++------------- doc/tutorial/test/JavascriptSpec.hs | 7 +- servant-js/src/Servant/JS.hs | 1 + 3 files changed, 105 insertions(+), 82 deletions(-) diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 09c21df3..4bc1466e 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -154,33 +154,33 @@ javascript with just a simple function call to `jsForAPI` from `Servant.JS`. ``` haskell -apiJS :: Text -apiJS = jsForAPI api jquery +apiJS1 :: Text +apiJS1 = jsForAPI api jquery ``` This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks': ``` javascript -var getPoint = function (onSuccess, onError) - { - $.ajax( - { url: '/point' - , success: onSuccess - , error: onError - , method: 'GET' - }); -}; +var getPoint = function(onSuccess, onError) +{ + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , type: 'GET' + }); +} -var getBooks = function (q, onSuccess, onError) - { - $.ajax( - { url: '/books' + '?q=' + encodeURIComponent(q) - , success: onSuccess - , error: onError - , method: 'GET' - }); -}; +var getBooks = function(q, onSuccess, onError) +{ + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , type: 'GET' + }); +} ``` We created a directory `static` that contains two static files: `index.html`, @@ -192,7 +192,7 @@ write the generated javascript into a file: ``` haskell writeJSFiles :: IO () writeJSFiles = do - T.writeFile "static/api.js" apiJS + T.writeFile "static/api.js" apiJS1 jq <- T.readFile =<< Language.Javascript.JQuery.file T.writeFile "static/jq.js" jq ``` @@ -220,7 +220,7 @@ The `CommonGeneratorOptions` will let you define different behaviors to change how functions are generated. Here is the definition of currently available options: -```haskell +```haskell ignore data CommonGeneratorOptions = CommonGeneratorOptions { -- | function generating function names @@ -249,8 +249,8 @@ Use the same code as before but simply replace the previous `apiJS` with the following one: ``` haskell -apiJS :: String -apiJS = jsForAPI api vanillaJS +apiJS2 :: Text +apiJS2 = jsForAPI api vanillaJS ``` The rest is *completely* unchanged. @@ -259,41 +259,50 @@ The output file is a bit different, but it has the same parameters, ``` javascript -var getPoint = function (onSuccess, onError) + +var getPoint = function(onSuccess, onError) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', '/point', true); - - xhr.onreadystatechange = function (e) { + 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); - if (xhr.status == 200 || xhr.status == 201) { - onSuccess(value); - } else { - onError(value); - } - } + onSuccess(value); + } else { + var value = JSON.parse(xhr.responseText); + onError(value); + } } - xhr.send(null); -}; + } + xhr.send(null); +} -var getBooks = function (q, onSuccess, onError) +var getBooks = function(q, onSuccess, onError) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true); - - xhr.onreadystatechange = function (e) { + 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); - if (xhr.status == 200 || xhr.status == 201) { - onSuccess(value); - } else { - onError(value); - } - } + onSuccess(value); + } else { + var value = JSON.parse(xhr.responseText); + onError(value); + } } - xhr.send(null); -}; + } + xhr.send(null); +} + + ``` And that's all, your web service can of course be accessible from those @@ -309,8 +318,8 @@ Use the same code as before but simply replace the previous `apiJS` with the following one: ``` haskell -apiJS :: String -apiJS = jsForAPI api $ axios defAxiosOptions +apiJS3 :: Text +apiJS3 = jsForAPI api $ axios defAxiosOptions ``` The rest is *completely* unchanged. @@ -319,19 +328,23 @@ The output file is a bit different, ``` javascript -var getPoint = function () -{ - return axios({ url: '/point' - , method: 'get' - }); -}; -var getBooks = function (q) +var getPoint = function() { - return axios({ url: '/books' + '?q=' + encodeURIComponent(q) - , method: 'get' - }); -}; + return axios({ url: '/point' + , method: 'get' + }); +} + + + +var getBooks = function(q) +{ + return axios({ url: '/books' + '?q=' + encodeURIComponent(q) + , method: 'get' + }); +} + ``` **Caution:** In order to support the promise style of the API, there are no onSuccess @@ -366,8 +379,8 @@ generate top level functions. The difference is that `angular` Generator always takes an argument. ``` haskell -apiJS :: String -apiJS = jsForAPI api $ angular defAngularOptions +apiJS4 :: Text +apiJS4 = jsForAPI api $ angular defAngularOptions ``` The generated code will be a bit different than previous generators. An extra @@ -379,21 +392,25 @@ nor onError callback functions. ``` javascript + var getPoint = function($http) { return $http( - { url: '/counter' - , method: 'GET' - }); + { url: '/point' + , method: 'GET' + }); } + + var getBooks = function($http, q) { return $http( - { url: '/books' + '?q=' + encodeURIComponent(q), true); + { url: '/books' + '?q=' + encodeURIComponent(q) , method: 'GET' }); } + ``` You can then build your controllers easily @@ -440,8 +457,8 @@ app.service('MyService', function($http) { To do so, you just have to use an alternate generator. ``` haskell -apiJS :: String -apiJS = jsForAPI api $ angularService defAngularOptions +apiJS5 :: Text +apiJS5 = jsForAPI api $ angularService defAngularOptions ``` Again, it is possible to customize some portions with the options. @@ -468,32 +485,34 @@ Servant comes with three name builders included: Keeping the JQuery as an example, let's see the impact: ``` haskell -apiJS :: String -apiJS = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase } +apiJS6 :: Text +apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase } ``` -This `String` contains 2 Javascript functions: +This `Text` contains 2 Javascript functions: ``` javascript -var getPoint = function (onSuccess, onError) + +var get_point = function(onSuccess, onError) { $.ajax( { url: '/point' , success: onSuccess , error: onError - , method: 'GET' + , type: 'GET' }); -}; +} -var getBooks = function (q, onSuccess, onError) +var get_books = function(q, onSuccess, onError) { $.ajax( { url: '/books' + '?q=' + encodeURIComponent(q) , success: onSuccess , error: onError - , method: 'GET' + , type: 'GET' }); -}; +} + ``` diff --git a/doc/tutorial/test/JavascriptSpec.hs b/doc/tutorial/test/JavascriptSpec.hs index 2d6007a5..7dfd4cec 100644 --- a/doc/tutorial/test/JavascriptSpec.hs +++ b/doc/tutorial/test/JavascriptSpec.hs @@ -15,7 +15,10 @@ spec = do describe "apiJS" $ do it "is contained verbatim in Javascript.lhs" $ do code <- readFile "Javascript.lhs" - cs apiJS `shouldSatisfy` (`isInfixOf` code) + cs apiJS1 `shouldSatisfy` (`isInfixOf` code) + cs apiJS3 `shouldSatisfy` (`isInfixOf` code) + cs apiJS4 `shouldSatisfy` (`isInfixOf` code) + cs apiJS6 `shouldSatisfy` (`isInfixOf` code) describe "writeJSFiles" $ do it "[not a test] write apiJS to static/api.js" $ do @@ -24,7 +27,7 @@ spec = do describe "app" $ with (return app) $ do context "/api.js" $ do it "delivers apiJS" $ do - get "/api.js" `shouldRespondWith` (fromString (cs apiJS)) + get "/api.js" `shouldRespondWith` (fromString (cs apiJS1)) context "/" $ do it "delivers something" $ do diff --git a/servant-js/src/Servant/JS.hs b/servant-js/src/Servant/JS.hs index 498d45da..d11c6eb0 100644 --- a/servant-js/src/Servant/JS.hs +++ b/servant-js/src/Servant/JS.hs @@ -112,6 +112,7 @@ module Servant.JS , javascript , NoTypes , GenerateList(..) + , FunctionName(..) ) where import Prelude hiding (writeFile) From b22e4e72f968bcbad8dff9cee1b58cbc4275e884 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Mon, 15 Aug 2016 15:39:07 -0300 Subject: [PATCH 4/4] Review fixes --- doc/tutorial/Javascript.lhs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 4bc1466e..2d9ab6bd 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -224,17 +224,17 @@ available options: data CommonGeneratorOptions = CommonGeneratorOptions { -- | function generating function names - functionNameBuilder :: FunctionName -> String + functionNameBuilder :: FunctionName -> Text -- | name used when a user want to send the request body (to let you redefine it) - , requestBody :: String + , requestBody :: Text -- | name of the callback parameter when the request was successful - , successCallback :: String + , successCallback :: Text -- | name of the callback parameter when the request reported an error - , errorCallback :: String + , errorCallback :: Text -- | namespace on which we define the js function (empty mean local var) - , moduleName :: String + , moduleName :: Text -- | a prefix that should be prepended to the URL in the generated JS - , urlPrefix :: String + , urlPrefix :: Text } ``` @@ -363,9 +363,9 @@ data AxiosOptions = AxiosOptions -- should be made using credentials withCredentials :: !Bool -- | the name of the cookie to use as a value for xsrf token - , xsrfCookieName :: !(Maybe String) + , xsrfCookieName :: !(Maybe Text) -- | the name of the header to use as a value for xsrf token - , xsrfHeaderName :: !(Maybe String) + , xsrfHeaderName :: !(Maybe Text) } ``` @@ -466,11 +466,11 @@ Again, it is possible to customize some portions with the options. ``` haskell data AngularOptions = AngularOptions { -- | When generating code with wrapInService, name of the service to generate, default is 'app' - serviceName :: String + serviceName :: Text , -- | beginning of the service definition - prologue :: String -> String -> String + prologue :: Text -> Text -> Text , -- | end of the service definition - epilogue :: String + epilogue :: Text } ```