Annotations
Since 1.3
Overview
Together with 1.3 annotation mechanism has been introduced. In short, Erlang Web annotations allow developer to separate the business logic of the target function from different kind, non-domain specific wrappers, such as validators, converters, loggers, authenticators and so on.
Assume we want to call the controller function blog:post(BlogPost). But before doing the actual posting, we would like to do the following things:
- authorization
- post content validation
- logging the activity
After the successful posting we would like to do:
- invalidation of the cache entries that hold the most recent posts
- notify the mailing list
Of course we can use dataflow mechanism in order to separate those activities from the actual blog:post function's body, but unfortunately, dataflow has some major drawbacks:
- we must specify the dataflow clause for all controller functions, even if we do not want to perform any before/after operation on them
we must create error/2 function that in some cases is not needed
- the last function on the dataflow list must return a list of arguments, which length is equal to the arity of the target function - so we must watch out for dataflow list reordering
- all of the functions that are placed on the dataflow list must be a exported from the target function module (so the number of exports starts to grow up very fast)
- we can't create one "utility" module that contains all the wrappers: loggers, validators, error handlers
- looking at the function definition we can't say what will be called before and after that function, since the dataflow function must be declared only once and it deals with all controller functions
The annotation mechanism get rid off all of those disadvantages and helps developers to write cleaner and more understandable code.
There are two types of Erlang Web's annotations: before and after. before annotations are called before the actual function execution, after are triggered when the target function ends.
Syntax
In order to annotate the function (that has been mentioned in the previous section) we must use the following syntax:
1 -module(blog).
2
3 -export([post/1]).
4
5 -include_lib("blog/include/utils_annotation.hrl").
6
7 ...
8
9 %% Before annotations
10 ?AUTHORIZE(editor).
11 ?VALIDATE(blog_post).
12 ?LOG({?MODULE, post}).
13
14 %% After annotations
15 ?INVALIDATE(["post", "^/index\.html$", "recent"]).
16 ?NOTIFY_ML(new_post).
17 post(BlogPost) ->
18 wtype_post:create(BlogPost),
19
20 {template, "blog/post/successful.html"}.
As we can see on the example above, the annotations are simply the macros put right before the function definition (the annotation is bound the first function that is declared below it). The macros definitions are put inside the header file (in this case it is utils_annotations.hrl), which is autogenerated during the compile phase.
The order of the annotations does matter: the functions corresponding to the annotations will be triggered in the same order as the order of the annotations. Although it is possible to mix the before and after annotations (the order authorize, validate, log, invalidate, notify_ml is the same as authorize, invalidate, validate, notify_ml, log) it is not recommended, since it leads to misunderstands and is error-prone.
Moreover, in order to recognize the before and after annotations, if their names do not exactly specify what kind of annotation is it, developers, when creating the annotations, can prefix/suffix their names with the proper marker (like LOG_BEFORE and INVALIDATE_AFTER).
The only limitation that is put on the annotated functions (target functions) is that their signatures must not contain bare underscore jokers: _. So we can annotate:
1 ?LOG(hello).
2 test(_Args) ->
3 {template, "test.html"}.
but we can't:
1 ?LOG(hello).
2 test(_) ->
3 {template, "test.html"}.
Specification
Erlang Web framework provides an easy way to define our own annotations and use them all across the project. But before we will see the annotation creation tutorial, let's look at the specification.
Annotation is simply an Erlang function that is annotated with some special, built-in annotations, that specify what kind of annotation it is (before or after). Let's take a look at the function signature:
1 ?BEFORE. %% ?AFTER.
2 annotation_name(AnnotationArg, TargetModule, TargetFunction, TargetFunctionArgs | TargetFunctionResult) -> Result
3
4 AnnotationArg :: term()
5 TargetModule, TargetFunction :: atom()
6 TargetFunctionArgs :: list(term())
7 TargetFunctionResult :: term()
8
9 Result :: {proceed, NewFunctionArgs | NewResult} | {skip, NewResult} | {error, {ErrorMod, ErrorFun, ErrorArgs}}
10 NewFunctionArgs :: list(term())
11 NewResult :: term()
12 ErrorMod, ErrorFun :: atom()
13 ErrorArgs :: list(term())
The AnnotationArg is simply the parameter we pass to the macro when we call annotate some function (such as ["post", "^/index\.html$", "recent"] in ?INVALIDATE(["post", "^/index\.html$", "recent"]).). It can be an arbitrary Erlang term.
A pair TargetModule:TargetFunction and the length of the TargetFunctionArgs exactly determines the function that we are annotating (in the previous example: blog:post/1).
The last parameter of the function depends on the type of the annotation. When the annotation is of type before it becomes a TargetFunctionArgs - a list of arguments that will be passed to the target function. The annotation can modify the elements that are on the list but should not change its length (add/remove elements - it should only replace them). Otherwise, when the function is an after annotation it becomes a TargetFunctonResult - an Erlang term that has been returned from the target function.
Now let's take a look at the function result value. Function that is going to be an annotation must return one of the following values:
{proceed, NewTargetFunctionArgs | NewResult} - the annotation function has ended properly, the call flow can proceed. The next function on the annotation list will be called (but with NewTargetFunctionArgs instead of TargetFunctionArgs (or NewResult instead of TargetFunctionResult). If there are no annotations left, the target function will be evaluated (in case of before annotation type) or the result will be returned (after).
{skip, Result} - the call flow will be broken - neither no further annotations will be called, nor the target function. The Result will be returned to the caller (but from its point of view function has evaluated properly)
{error, {ErrorMod, ErrorFun, ErrorArgs} - an error has occurred, the passed error handler will be called (apply(ErrorMod, ErrorFun, ErrorArgs)). A return value from the error handler will be treated as a return value from the target function. No further annotations will be called.
Built-in annotations
Erlang Web framework is distributed together with some pre-defined annotations:
INVALIDATE, INVALIDATE_IF, INVALIDATE_GROUPS, INVALIDATE_GROUPS_IF - are described on EpticFE wiki page
BACKEND_CALL - in case of running the system in the distributed environment (one node plays backend role, other nodes are frontends) it is possible to annotate some functions that must be executed on the backend side. When one of that functions is called, the RPC call to the frontend node to that function is performed.
Defining new annotation
The proper tutorial is here.
