Tutorial Step 1 - Project Setup
The goal of the first tutorial step is to write a service that will provide the basic functionality for browsing the items that are available in the web shop.
Contents
Setting up the environment
To set up the environment with rebar and the latest Erlang Web release visit http://wiki.erlang-web.org/Rebar
About rebar: https://bitbucket.org/basho/rebar/wiki/Home
At first we must download the Erlang Web 1.3 release (it can be done by cloning the official Mercurial repository):
$ hg clone https://bitbucket.org/etc/erlang-web/
Then we should compile the sources and prepare the project environment:
$ cd erlang-web/ $ ./bin/compile.erl $ ./bin/start.erl
At the beginning, our destination server will be Inets (but note that change of the web server is very easy - e.g. bin/compile.erl release 0.2 yaws will switch it).
Our workplace should now look like:
bin/ doc/ Emakefile lib/ log/ releases/ config/ docroot/ INSTALL LICENSE pipes/ templates/
Let's start the server and check out if the installation ended successfully:
./bin/start_interactive
The server by default is listening on the port 8080 - it could be changed by modifying the config/inets.conf file.
Enter the http://localhost:8080/ in your browser and you should see:
The meaning of the particular directory is described here
Adding new application
Each Erlang Web project consists of at least five non-OTP applications: eptic, eptic_fe (in some cases it remains unused), wpart, wparts + additional, user implemented one.
Everything is working correctly, let's create a new application:
./bin/add.erl Name of your application: shop Version of your application [0.1]: Element created: lib/shop-0.1 Element created: lib/shop-0.1/doc Element created: lib/shop-0.1/ebin Element created: lib/shop-0.1/include Element created: lib/shop-0.1/priv Element created: lib/shop-0.1/src Element created: lib/shop-0.1/ebin/shop.app Element updated: Emakefile
The bin/add.erl script has created a directory tree for our shop application and updated also the main Emakefile so it will be possible to compile the whole project using only one command (bin/compile.erl).
Creating an item model
Erlang Web framework uses a slightly modified MVC pattern for dealing with the data (model), outlook (view) and the business logic (controller). Our first step towards the web shop is to define the model that will be responsible for an item. Assume, for the beginning, that our model contains the following fields:
- id - integer, our primary key
- name - string - short title for the item
- description - text - longer item's description
- price - float - price for a unit
- available - enum - the availability of the item
The model's description should be put in the header files (.hrl). Each model should be described with two records: first - the one that will be used for object manipulation (e.g. storing in the database) and one for object definition - types of the particular fields, the constraints, view modificators and so on. If the first record name is X then the latter one must be X_types.
We should now create new item.hrl file inside the lib/shop-0.1/include/ directory:
1 -record(item, {
2 id,
3 name = "",
4 description = "",
5 price = 0.0,
6 available = "true"
7 }).
8
9 -record(item_types, {
10 id = {integer, [primary_key, {private, true}]},
11 name = {string, [{max_length, 256}]},
12 description = {text, [{max_length, 10000}]},
13 price = {float, [{min, 0.0}]},
14 available = {enum, [{choices, "true:Yes|false:No"},
15 {chosen, "true"}]}
16 }).
As you can see, there are some basic constraints put on the particular fields: name's length can't be longer than 256 characters, description cannot be longer than 10000 and the price must not be a negative number. The page that is describing all the basic types can be found here.
Now we will use the Erlang Web generator for building a module that will be dealing with DBMS. However, although the naming conventions of that type of modules is (when model's name is X) wtype_X, though the generator script will change it for us:
$ ./bin/generate.erl model --name item --hrl lib/shop-0.1/include/item.hrl --app shop File (...)/lib/shop-0.1/src/wtype_item.erl created successfully!
In fact, we don't have to specify all of the named arguments to generate.erl script. It's smart enough to figure out --hrl using the convention and it will ask for application and model names interactively if those aren't provided.
Read generator script's documentation for more info.
Generator will create a wtype_item.erl file with some basic operations implemented. Moreover, it will update ebin/shop.app file as well:
1 -module(wtype_item).
2 -export([get_record_info/1]).
3 -export([create/1, read/1, update/1, delete/1]).
4 -export([prepare_initial/0, prepare_validated/0]).
5
6 -include("lib/shop-0.1/include/item.hrl").
7
8 get_record_info(item) -> record_info(fields, item);
9 get_record_info(item_types) -> #item_types{}.
10
11 create(Item) ->
12 e_db:write(item, Item).
13
14 read(all) ->
15 e_db:read(item);
16 read(Id) ->
17 e_db:read(item, Id).
18
19 update(Item) ->
20 e_db:update(item, Item).
21
22 delete(Id) ->
23 e_db:delete(item, Id).
24
25 prepare_initial() ->
26 wpart_db:build_record_structure(item, #item{}).
27
28 prepare_validated() ->
29 Item = wpart:fget("__not_validated"),
30 wpart_db:build_record_structure(item, Item).
Writing the database initializer
Let's also add the basic module that will be dealing with DB initializing process (put it inside the src directory but do not modify the .app since it will not be included in our final release). Its content should look like:
1 -module(db_init).
2 -export([init/0]).
3
4 -define(TABS, [item]).
5
6 -include("lib/shop-0.1/include/item.hrl").
7
8 init() ->
9 mnesia:stop(),
10 Node = node(),
11 case mnesia:create_schema([Node]) of
12 ok ->
13 application:start(mnesia),
14 e_db:install(),
15
16 [install(Table) || Table <- ?TABS];
17 {error, {Node, {already_exists, Node} }} ->
18 application:start(mnesia),
19 error_logger:warning_msg("~p module, mnesia's schema already exists, "
20 "if you want to delete it, run mnesia:delete_schema/1~n",
21 [?MODULE]),
22 {error, schema_already_exists}
23 end.
24
25 install(Name) ->
26 mnesia:create_table(Name, [{attributes, (list_to_atom("wtype_" ++ atom_to_list(Name))):get_record_info(Name)},
27 {disc_copies, [node()]}]).
Creating a controller
Right now we are in the half way to success: now we should create a controller that would be responsible for handling the user's requests. Let's use generator once again:
$ ./bin/generate.erl controller --app shop --name browser Exported functions (separated with ,): show_item, list_all File (...)/lib/shop-0.1/src/browser.erl created successfully!
Generator updated the .app file too. The result browser.erl looks exactly like this:
1 -module(browser).
2 -export([show_item/1, list_all/1]).
3
4 show_item(_Args) ->
5 %% put the show_item function body here
6 ok.
7
8 list_all(_Args) ->
9 %% put the list_all function body here
10 ok.
Great! Now we must connect somehow the URLs with our controller functions. We must tell the framework's router how should it handle the incoming requests. This could be done by editing the config/dispatch.conf file (dispatcher is fully described here):
1 %%
2 %% SHOP PAGES
3 %%
4 {dynamic, "^/item/all$", {browser, list_all}}.
5 {dynamic, "^/item/show/(?<id>[0-9]+)$", {browser, show_item}}.
6
7 %%
8 %% ENTRIES FOR THE AUTOCOMPLETE WPART
9 %%
10 {static,"^/autocomplete.css$",enoent}.
11 {static,"^/jquery.autocomplete.js$",enoent}.
12 {static,"^/jquery.js$",enoent}.
13 {static,"^/indicator.gif$",enoent}.
14
15 %%
16 %% WELCOME PAGE
17 %%
18 {static,"^/?$","welcome.html"}.
We have added two new entries (the five remaining ones have been created during the project set up): one for displaying all entries that are stored in the database (available at /item/all) and one for rendering the information about particular item (/item/show/N where N is an integer). Moreover, dispatcher will pass a property list containing one element: [{id, N}] as a parameter to the browser:show_item/1.
Implementing item browser
Let's start implementing the controller! At first we should deal with displaying the particular element:
1 show_item(Args) ->
2 N = list_to_integer(proplists:get_value(id, Args)),
3 Item = wtype_item:read(N),
4 wpart:fset("item", wtype_item:format(Item)),
5
6 {template, "item/show.html"}.
Since there is no way to access the particular field of the record from the view, we must flatten the record and zip its name with its values. The wtype_item:format/1 function (do not forget to export it from the module):
1 ...
2
3 -export([format/1]).
4
5 ...
6
7 format(Item) ->
8 [{"id", Item#item.id},
9 {"name", Item#item.name},
10 {"description", Item#item.description},
11 {"price", Item#item.price},
12 {"available", if
13 Item#item.available == "true" ->
14 true;
15 true ->
16 false
17 end}].
Each of the controller function must return a value that is recognizable by server. For example we might want to render some template ({template, PathToTemplateFile}, redirect the user to some address ({redirect, URL}) or feed the browser with some controller-prepared data ({content, html, HTML}). The complete list of the values could be found here.
And the view (placed in templates/item/show.html):
<html>
<head>
<title>Erlang Web Shop - <wpart:lookup key="item:name" /></title>
</head>
<body>
<h1><wpart:lookup key="item:name" /></h1>
<wpart:choose>
<wpart:when test="{item:available}">
<h2>Item is not available!</h2>
</wpart:when>
</wpart:choose>
<p>Price: <wpart:lookup key="item:price" format="float(2)" /></p>
<p><wpart:lookup key="item:description" /></p>
</body>
</html>The view is in fact a regular XHTML file with some namespaces defined. First of them, and most commonly used - wpart is always expanded by the framework. When the template parser meets the wpart:name tag it calls the wpart_name:handle_call/1 function and passes a whole tag as an argument (it is a standard Xmerl structure). The second important namespace is wtpl which is used for templates inheritance. We will look at it in the later phase of the tutorial.
wpart:lookup tag fetches and inserts the value stored inside the request dictionary. Moreover, it can use a formatter for changing the value appearance.
wpart:choose provides an if functionality - it checks the conditions and renders only the content of this branch, that condition has evaluated to true as first.
The list of the ready-to-use wpart tags is here.
Next, we have to recompile and reload the changed modules. In order to do so, type eptic:reload(). into interactive console:
1 1> eptic:reload().
2 Recompile: (...)
3 ok
Before we will be able to run the example and check if it really works, we should create DB schema...
1 2> db_init:init(). % This should be done once and only once!
2 ok.
...and save some data in our database:
1 3> rr("lib/shop-0.1/include/item.hrl").
2 [item,item_types]
3 4> wtype_item:create(#item{name="Our first item", description="This is a very promising beginning!", price=123.99}).
4 ok
5 5> wtype_item:create(#item{name="The out of stock item", description="It was so desired product that it is no longer available", price=0.5, available="false"}).
6 ok
7 6> wtype_item:read(all).
8 [#item{id = 1,name = "Our first item",
9 description = "This is a very promising beginning!",
10 price = 123.99,available = "true"},
11 #item{id = 2,name = "The out of stock item",
12 description = "It was so desired product that it is no longer available",
13 price = 0.5,available = "false"}].
14 ok
Wow, we have two items in our database, so we should check them out from our browser:
http://localhost:8080/item/show/1
http://localhost:8080/item/show/2
Implementing item lister
Great, everything is working as it should. Let's move to the second controller function - listing all items that are stored inside the database:
1 list_all(_Args) ->
2 Items = wtype_item:read(all),
3 wpart:fset("items", lists:map(fun wtype_item:format/1, Items)),
4
5 {template, "item/show_all.html"}.
And the corresponding templates/item/show_all.html template:
<html>
<head>
<title>Erlang Web Shop - All Items</title>
</head>
<body>
<h1>All items that are stored in the shop:</h1>
<ul>
<wpart:list select="map" list="items" as="item">
<li>
<wpart:choose>
<wpart:when test="not {item:available}">
NOT AVAILABLE
</wpart:when>
</wpart:choose>
<a wpart:href="/item/show/{[integer]item:id}"><wpart:lookup key="item:name" /></a>
</li>
</wpart:list>
</ul>
</body>
</html>Ok - we are ready! Check the page:
http://localhost:8080/item/all
and see that everything we wanted to have has been achieved.
Download
This tutorial step is over, the package containing the sources of the application can be downloaded from here.
