Merge pull request #506 from haskell-servant/jkarni/servant-js-docs
Add more JS documentation.
This commit is contained in:
commit
9e7356c6d3
3 changed files with 318 additions and 24 deletions
|
@ -151,21 +151,120 @@ 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
|
||||
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
|
||||
, type: '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`,
|
||||
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 "static/api.js" apiJS1
|
||||
jq <- T.readFile =<< Language.Javascript.JQuery.file
|
||||
T.writeFile "static/jq.js" jq
|
||||
```
|
||||
|
||||
(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.
|
||||
|
||||
## 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 ignore
|
||||
data CommonGeneratorOptions = CommonGeneratorOptions
|
||||
{
|
||||
-- | function generating function names
|
||||
functionNameBuilder :: FunctionName -> Text
|
||||
-- | name used when a user want to send the request body (to let you redefine it)
|
||||
, requestBody :: Text
|
||||
-- | name of the callback parameter when the request was successful
|
||||
, successCallback :: Text
|
||||
-- | name of the callback parameter when the request reported an error
|
||||
, errorCallback :: Text
|
||||
-- | namespace on which we define the js function (empty mean local var)
|
||||
, moduleName :: Text
|
||||
-- | a prefix that should be prepended to the URL in the generated JS
|
||||
, urlPrefix :: Text
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
apiJS2 :: Text
|
||||
apiJS2 = 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.setRequestHeader("Accept","application/json");
|
||||
xhr.setRequestHeader(\"Accept\",\"application/json\");
|
||||
xhr.onreadystatechange = function (e) {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 204 || xhr.status == 205) {
|
||||
|
@ -186,7 +285,7 @@ var getBooks = function(q, onSuccess, onError)
|
|||
{
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
|
||||
xhr.setRequestHeader("Accept","application/json");
|
||||
xhr.setRequestHeader(\"Accept\",\"application/json\");
|
||||
xhr.onreadystatechange = function (e) {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 204 || xhr.status == 205) {
|
||||
|
@ -202,27 +301,218 @@ var getBooks = function(q, onSuccess, onError)
|
|||
}
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
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
|
||||
writeJSFiles :: IO ()
|
||||
writeJSFiles = do
|
||||
T.writeFile "static/api.js" apiJS
|
||||
jq <- T.readFile =<< Language.Javascript.JQuery.file
|
||||
T.writeFile "static/jq.js" jq
|
||||
apiJS3 :: Text
|
||||
apiJS3 = jsForAPI api $ axios defAxiosOptions
|
||||
```
|
||||
|
||||
(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`.
|
||||
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 Text)
|
||||
-- | the name of the header to use as a value for xsrf token
|
||||
, xsrfHeaderName :: !(Maybe Text)
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
apiJS4 :: Text
|
||||
apiJS4 = 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: '/point'
|
||||
, method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
var getBooks = function($http, q)
|
||||
{
|
||||
return $http(
|
||||
{ url: '/books' + '?q=' + encodeURIComponent(q)
|
||||
, 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
|
||||
apiJS5 :: Text
|
||||
apiJS5 = 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 :: Text
|
||||
, -- | beginning of the service definition
|
||||
prologue :: Text -> Text -> Text
|
||||
, -- | end of the service definition
|
||||
epilogue :: Text
|
||||
}
|
||||
```
|
||||
|
||||
# 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
|
||||
apiJS6 :: Text
|
||||
apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase }
|
||||
```
|
||||
|
||||
This `Text` contains 2 Javascript functions:
|
||||
|
||||
``` javascript
|
||||
|
||||
|
||||
var get_point = function(onSuccess, onError)
|
||||
{
|
||||
$.ajax(
|
||||
{ url: '/point'
|
||||
, success: onSuccess
|
||||
, error: onError
|
||||
, type: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
var get_books = function(q, onSuccess, onError)
|
||||
{
|
||||
$.ajax(
|
||||
{ url: '/books' + '?q=' + encodeURIComponent(q)
|
||||
, success: onSuccess
|
||||
, error: onError
|
||||
, type: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -112,6 +112,7 @@ module Servant.JS
|
|||
, javascript
|
||||
, NoTypes
|
||||
, GenerateList(..)
|
||||
, FunctionName(..)
|
||||
) where
|
||||
|
||||
import Prelude hiding (writeFile)
|
||||
|
|
Loading…
Reference in a new issue