Tutorial Step 4 - Using annotations
The goal of the first tutorial step is to show how Erlang Web's annotation can be used.
Contents
Defining the annotations
So far, we have implemented some basic web shop functionality. Unfortunately, as the time goes by, our controller functions will become bigger and bigger and at some point we will probably have to think longer and longer before we will figure our what is the real purpose of the particular function. For example, consider the potential outlook of the item_admin:do_edit/1 function:
1 do_edit(_Args) ->
2 case check_admin_permissions() of
3 true ->
4 case validate_tool:validate_cu(item, update) of
5 {ok, Item} ->
6 case check_item_existance(Item#item.id) of
7 true ->
8 wtype_item:update(Item),
9 my_logger:log(info, {add, item}),
10 {redirect, "/item/all"};
11 ...
In the future, when we will be ready to add the caching engine, we will have to invalidate the content as well. Moreover, when we will distribute the service we will have to ensure that the process executes on the backend side (where DBMS is deployed). Each of the checks will cause the error handlers clauses: The code will become more and more dirty and unreadable.
To avoid this let's use Erlang Web's annotations. At the beginning we should define only the narrow set of them: in the future we will add new and new ones.
Creating annotations' definitions
The annotation engine has been introduced here.
Let's create a new module called shop_utils.erl:
1 -module(shop_utils).
2
3 -export([validate/4]).
4 -export([check_existence/4]).
5
6 -include_lib("eptic/include/e_annotation.hrl").
7
8 ?BEFORE.
9 validate({Model, Type}, Mod, Fun, _Args) ->
10 case validate_tool:validate_cu(Model, Type) of
11 {ok, Item} ->
12 {proceed, [Item]};
13 {error, _Reason} ->
14 {error, {Mod, validate_error, [Fun]}}
15 end.
16
17 ?BEFORE.
18 check_existence({id, Model}, Mod, _Fun, [Args]) ->
19 Id = list_to_integer(proplists:get_value(id, Args)),
20 case (list_to_atom("wtype_" ++ atom_to_list(Model))):read(Id) of
21 not_found ->
22 {error, {Mod, element_not_found, [Id]}};
23 Element ->
24 {proceed, [Element]}
25 end;
26 check_existence({element, Model}, Mod, _Fun, [Element]) ->
27 Id = element(2, Element),
28 case (list_to_atom("wtype_" ++ atom_to_list(Model))):read(Id) of
29 not_found ->
30 {error, {Mod, element_not_found, [Id]}};
31 _ ->
32 {proceed, [Element]}
33 end.
First annotation, validate will check if the data user entered are correct. If so - the annotation will allow to call either next annotation or the actual, target function. Otherwise it will call the validate_error function from the target function's module.
The second annotation behaviour, check_existence, depends on the call context. In case we are passing the bare id of the element it checks if the element exists in the database. Otherwise, when we pass the whole element as an argument, it checks if it actually resides in the DB.
Since we have created this module by hand, do not forget to add it to the .app file (modules section should now look like: {modules,[item_admin,browser,wtype_item,shop_utils]}).
What is important - the annotation definition modules must be compiled before the modules where the annotations are actually used. This is so because during the compliance process Erlang compiler will create a special header file that will contain the macros definitions and the parse_transform attribute.
Let's compile the annotations:
$ ./bin/compile.erl
We should get a new header file in the include directory in our shop application: shop_utils_annotations.hrl:
1 -compile({parse_transform, e_user_annotation}).
2 -compile(nowarn_shadow_vars).
3
4 -define(VALIDATE(Args), -ew_user_annotation({Args, before, shop_utils, validate})).
5
6 -define(CHECK_EXISTENCE(Args), -ew_user_annotation({Args, before, shop_utils, check_existence})).
We should not modify it - we will only include and use it in our controllers. As the time goes by this file will grow up.
Modifying the controller
Now it is a time for changing the controller functions. But before doing it, we should create two new functions: validate_error/1 and element_not_found/1 - the error handlers for the annotations:
1 %% error handlers
2 -export([validate_error/1, element_not_found/1]).
3
4 ...
5
6 validate_error(do_add) ->
7 wpart:fset("__edit", wtype_item:prepare_validated()),
8 {template, "item/add.html"};
9 validate_error(do_edit) ->
10 wpart:fset("__primary_key", list_to_integer(wpart:fget("post:__primary_key"))),
11 wpart:fset("__edit", wtype_item:prepare_validated()),
12 {template, "item/edit.html"}.
13
14 element_not_found(Id) ->
15 wpart:fset("id", Id),
16 {template, "item/not_found.html"}.
First function will be responsible for rendering the add/edit page when user enter the incorrect data. The second one will display the page with the information about the not-found element.
After adding the error handlers we are ready to use our annotations. To do this we must do the following things:
include the shop_utils_annotations.hrl file
- annotate the desired functions
- remove the unnecessary checks from the function bodies
The resulting file should now look like:
1 -module(item_admin).
2
3 -export([do_add/1, delete/1]).
4 -export([edit/1, do_edit/1]).
5
6 %% error handlers
7 -export([validate_error/1, element_not_found/1]).
8
9 -include("shop_utils_annotations.hrl").
10
11 ?VALIDATE({item, create}).
12 do_add(Item) ->
13 wtype_item:create(Item),
14
15 {redirect, "/item/all"}.
16
17 ?CHECK_EXISTENCE({id, item}).
18 delete(Item) ->
19 wtype_item:delete(element(2, Item)),
20
21 {redirect, "/item/all"}.
22
23 ?CHECK_EXISTENCE({id, item}).
24 edit(Item) ->
25 wpart:fset("__edit", wpart_db:build_record_structure(item, Item)),
26 wpart:fset("__primary_key", element(2, Item)),
27
28 {template, "item/edit.html"}.
29
30 ?VALIDATE({item, update}).
31 ?CHECK_EXISTENCE({element, item}).
32 do_edit(Item) ->
33 wtype_item:update(Item),
34
35 {redirect, "/item/all"}.
36
37 validate_error(do_add) ->
38 wpart:fset("__edit", wtype_item:prepare_validated()),
39 {template, "item/add.html"};
40 validate_error(do_edit) ->
41 wpart:fset("__edit", wtype_item:prepare_validated()),
42 {template, "item/edit.html"}.
43
44 element_not_found(Id) ->
45 wpart:fset("id", Id),
46 {template, "item/not_found.html"}.
Compile the module by running:
$ ./bin/compile.erl
The last thing to do is to create a item/not_found.html template:
<html> <head> <title>Not found</title> </head> <body> <h2>Item with ID = <wpart:lookup key="id" format="integer" /> does not exist!</h2> </body> </html>
Testing the service
Now we should check if the annotations do the things we supposed them to do:
- try to add the incorrect data
should result in:
- try to remove not existing item (with ID = 1234):
Great! Everything works as it should - let's then proceed to the next part of the tutorial.
Download
This tutorial step is over, the package containing the sources of the application can be downloaded from here.
