Step number 2
Introduction
In this step we will add the dispatcher and dataflow feature to our application.
Skills we will learn
- Using dispatcher
- Using dataflow function
Tutorial
As we could notice, the addresses, which have forms like /app/mod/fun are unfriendly, hard to remember and to type. In addition, they reveal the internal logical structure of our service, which sometimes is not a good idea. To avoid that defects the dispatcher has been created.
The main role of the dispatcher is to route the incoming requests to the proper controllers or directly to the views and to provide the user friendly URLs. What's more - we are getting the RESTful style of addressing completely for free.
NOTE: modifying dispatch.conf does not involve changing the code! At the beginning we put only those two configuration lines there:
{dynamic, "^/shop/list", {shop, list}}.
{dynamic, "^/shop", {shop, list}}.
The each tuple means:
- {dynamic, "^/shop/list", {shop, list}} - the request will be handled by the controller
- {dynamic, "^/shop/list", {shop, list}} - this rule will apply to all the requests, which destination address matches the given regular expression
- {dynamic, "^/shop/list", {shop, list}} - the request will be passed to the specified {Module, Function}
To make life simplier we assume that when the user enters the /shop address - he wants the list of the items we are selling.
Now, when we have those lines in our dispatch.conf file (placed in config/ directory), we are ready to reload the configuration.
This could be simply done by entering e_dispatcher:reinstall() in the Erlang shell.
Let's try it out: click here or here.
Ok - but what's with RESTful addressing style?
What we want to achieve is to provide users the possibility of controlling, how many items are listed on the main page.
For example, if user wants to see first ten products, he should go to the /shop/list/10.
In other case, when he wants only the first one: /shop/list/1.
We should also think about some kind of "default" actions, like: /shop/list or /shop passing without any parameters.
Moreover we don't want users to crash our service by typing the invalid addresses in their browsers: /shop/list/crash.
So now we have dillema - we have two functions: obtaining and validating passed arguments and rendering the actual content. For that reason, by separating the actual controller objective from the generic actions like validating/authorising/logging/etc. we will enable the dataflow mechanism.
To do that, we have to export two main functions:
-export([dataflow/1, error/2]).
which are responsible for defining the data flow in our controller and for handling errors.
The first one (dataflow/1) takes the one argument - name of the function which we want to call - and returns the list of functions which should be called before the final one.
In our case, it should look like:
dataflow(list) -> [prepare_args].
That means, if we want to call shop:list, shop:prepare_args has to be called first.
There could be more functions on list we are returning.
The dataflow engine calls them one-by-one and if the function returns the tuple {ok, ListOfArgs}, we are calling the next one with two arguments: the name of the final function and the list of the arguments we returned from the prior function.
Otherwise, if we return {error, Reason} the chain of calls will be broken and we will call the corresponding error/2 function.
The second mandatory function is error/2 which takes the name of the function we were to call (the error occured inside of the function specified in the dataflow) and the error description.
Knowing all the details we are ready to change our shop module.
Let's start with the function which is responsible for preparing the arguments for our list:
prepare_args(list, _) ->
Url = wpart:fget("__path"),
Parts = string:tokens(Url, "/"),
case length(Parts) of
1 ->
{ok, [5]};
2 ->
{ok, [5]};
3 ->
[_, _, N] = Parts,
case catch list_to_integer(N) of
Int when is_integer(Int) ->
{ok, [Int]};
_ ->
{error, bad_arg}
end;
_ ->
{error, bad_arg}
end.
Our function takes two arguments: the name of the function we want to call at the end of the dataflow chain (list) and the arguments returned from the previous dataflow function.
Because it is the first function in sequence, we skip the second argument (which will be an empty list).
At the beginning we get the URL we are serving (it is stored in the request dictionary under the "__path" key) and we split it by "/".
Then we check its length.
If someone has typed the "short" version (default one), the length of the URL parts will be less than 3.
In that case it will present only first five products (by returning {ok, [5]}.
If the list's length equals 3 - we check if the last argument is a number - if so shows the requested number of products.
Otherwise it returns {error, bad_arg} tuple and breaks the dataflow function calls chain (the user either typed a very long address - like "/shop/list/10/4" or the last part of the URL is not a number - like "/shop/list/latest").
Let's take a look on the error function:
error(list, bad_arg) ->
{template, "bad_arg.html"}.
Wow - when we get an bad_arg error when we wanted to serve list, we will simply expand the template "bad_arg.html"!
Function responsible for handling errors is a normal controller function - it can return templates, redirects, errors and so on.
The error page is simple as well:
<wtpl:parent path="templates/base.html">
<wtpl:content name="content">
<h3>Item not found!</h3>
</wtpl:content>
</wtpl:parent>
The last thing we want to change is the actual list:
list([N]) ->
AllItems = e_db_mnesia:read(shop),
LimitedItems = lists:sublist(lists:reverse(AllItems), N),
wpart:fset("items", LimitedItems),
{template, "list.html"}.
What has changed?
First, we are taking the one argument - list of arguments returned from the last dataflow function.
The second change is that we split the list of items on Nth position - which is provided by the prepare_args function.
Everything is ready now - we can check the effects of our work clicking here and here.
To check the error page click here.
