Docendo discimus

$self->explain

  • Calendar

    May 2017
    M T W T F S S
    « Sep    
    1234567
    891011121314
    15161718192021
    22232425262728
    293031  
  • Archives

  • Recent Posts

  • Bling

Posts Tagged ‘actions’

Catalyst is easy – rondo alla API

Posted by brunorc on June 21, 2009

I’ve already shown that Catalyst is able to process some user input. Really! And not only process, it can even discover if the form was really filled. And since you’re going to write web applications, you probably know that there is no spoon, no browser and no user on the other side of the request. Or, at least, there may be no user.

And if there is no user, what’s the sense of showing the form? There may be another action, designed especially for non-human users. Or, to avoid being accused of racism – some formless users.

This action needs to make use of another attribute – Args – because it needs to receive one argument, which would be the string that is expected to be added to the list. The minimal code for this action could look like this:

sub add :Local :Args(1) {
    my ( $self, $c, $newitem ) = @_;

    push @items, $newitem;
}

except it wouldn’t work, because every action has to have distinct name; api_add looks like a nice name, so after renaming the action we can try.

We can try to see the error message.

OK, that was intentional. But if we are here, why not to spend a while looking at it and try to understand it? First there’s an error message, that informs us that Catalyst could not find a view to forward to. But the application has no view at the moment, so it is not strange Catalyst couldn’t have found it. Anyway, in case it could help us resolving the error, Catalyst shows us four important objects it keeps in its context: Request and Response (which were mentioned in the previous part) along with Stash and Config. While Config sounds rather obvious – it apparently holds the configuration data for the application – Stash requires some explanation: it is just a big bag we can use for the data used while creating the Response.

This problem can be solved if there’s some output, passed to the body:

sub api_add :Local :Args(1) {
    my ( $self, $c, $newitem ) = @_;

    push @items, $newitem;

    $c->res->body("OK");
}

Now it works, but the code pushing the new item into list is doubled. What if there is a new requirement which says “no underscore in item names, and treat spaces as separators”? This code should be isolated, and both actions should only act as wrappers. As always, Catalyst makes it easy:

sub _add :Private {
    my ( $self, $c ) = @_;

    if ( $c->stash->{newitem} =~ s/_//g ) {
        $c->stash->{message} = 'Removed illegal underscore(s). ';
    }

    my @new_items = split / /, $c->stash->{newitem};

    push @items, @new_items;

    $c->stash->{count} = @new_items;
}

There is a new action attribute: Private. It means that this action cannot be called directly by using URL, it is reachable only from other actions. And since those actions call the _add action, they have to pass some parameters and – last but not least – they would probably like to know what happened. Hence the use of stash; action gets its parameter newitem from the stash, and then populates the stash with an optional message and the information about the count of added items.

sub api_add :Local :Args(1) {
    my ( $self, $c, $newitem ) = @_;

    $c->stash->{newitem} = $newitem;

    $c->forward('_add');

    $c->res->body( $c->stash->{count} ? "OK" : "ER" );
}

Now the api_add action doesn’t do much: it just packs the $newitem into stash and then creates some response, based on the count of added objects. It also uses the forward method to call the private action. The same will happen to the add action:

sub add :Local :Args(0) {
    my ( $self, $c ) = @_;

    my $optional;

    if ( $c->req->params->{newitem} ) {
        $c->stash->{newitem} = $c->req->params->{newitem};

        $c->forward('_add');

        if ( $c->req->params->{addit} eq 'Add it!' and $c->stash->{count} ) {
            my $count = $c->stash->{count};
            $optional = "<h4>Thank you for adding $count items!</h4>";
        }

        if ( $c->stash->{message} ) {
            $optional .= '<h4>' . $c->stash->{message} . '</h4>';
        }
    }

    my $template = qq[
<html>
    <head>
        <title>Add new item</title>
    </head>
    <body>
        <div align="center">
            <h3>Add something!</h3>
            <form method="get">
                <input type="text" size="24" id="newitem" name="newitem">
                <input type="submit" id="addit" name="addit" value="Add it!">
            </form>
            $optional
        </div>
    </body>
</html>
];

    $c->response->body( $template );
}

Although the forward method passes the @_ list as well as the whole $c object, it is a good idea to extract the newitem and store it into stash, instead of doing it inside the _add action. Again, this is the separation of concerns – wrapper unwraps (isn’t it logical?) the parameters, while the actual action just acts on well defined content of the stash.

Coda

The whole thing about Args attribute and the API stuff were quite nice, but pretty wrong. Now the application has an action that changes its state (in other words – the action has some side effects), and this action is accessible using the GET request. What if someone bookmarks this URL and then use it repeatedly? It can of course be solved by not allowing to insert the same item twice (how?), but that way we handle the effect, not the cause. That said, the action can be improved easily:

sub api_add :Local :Args(1) {
    my ( $self, $c, $newitem ) = @_;

    if ( $c->req->method eq 'POST' ) {
        $c->stash->{newitem} = $newitem;

        $c->forward('_add');

        $c->res->body( $c->stash->{count} ? "OK" : "ER" );
    }
    else {
        $c->res->status(405);
        $c->res->body('ER');
    }
}

Status code 405 means “Method not allowed”, so it is perfect for this situation. To check the if the code works as expected we need something more than a browser. There is a command line utility called lwp-request, which comes with Perl LWP module and even has some documentation, so I’ll just use it:

$ lwp-request -se http://localhost:3000/api_add/beer          
405 Method Not Allowed
Connection: close
Date: Sun, 21 Jun 2009 17:06:41 GMT
Content-Length: 2
Content-Type: text/html; charset=utf-8
Client-Date: Sun, 21 Jun 2009 17:06:41 GMT
Client-Peer: 127.0.0.1:3000
Client-Response-Num: 1
Status: 405
X-Catalyst: 5.80005

ER

Fails as expected! Now the proper incantation (when lwp-request asks for entering content, just press Ctrl D):

$ lwp-request -se -m POST http://localhost:3000/api_add/beer
Please enter content (application/x-www-form-urlencoded) to be POSTed:
200 OK
Connection: close
Date: Sun, 21 Jun 2009 17:06:53 GMT
Content-Length: 2
Content-Type: text/html; charset=utf-8
Client-Date: Sun, 21 Jun 2009 17:06:53 GMT
Client-Peer: 127.0.0.1:3000
Client-Response-Num: 1
Status: 200
X-Catalyst: 5.80005

OK

This is rather coarse way of testing the application. But Catalyst also provides one with sophisticated testing tools, however it is a different topic.

Posted in Catalyst for intimidated | Tagged: , , , , , , , , , | Leave a Comment »

Catalyst is easy, second movement

Posted by brunorc on June 21, 2009

After some digressions I get back to the main course – the Catalyst itself. I promised to show something more dynamic, so here we go.

It is obvious that at some point application needs to get some input from the user. It is usually done by showing the form to the user, and then using the data user puts into the form. Showing the form should be rather easy – let’s start from copying “header” and “footer” of the index action, renaming the action to add:

sub add :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->response->body( $template );
}

Of course the Catalyst server was ran with the -r flag, this code will die immediately, since the variable $template was not defined. This can be easily solved by putting some HTML code in the mentioned variable:

sub add :Path :Args(0) {
    my ( $self, $c ) = @_;

    my $template = qq[
<html>
    <head>
        <title>Add new item</title>
    </head>
    <body>
        <div align="center">
            <h3>Add something!</h3>
            <form method="get">
                <input type="text" size="24" id="newitem" name="newitem">
                <input type="submit" id="addit" name="addit" value="Add it!">
            </form>
        </div>
    </body>
</html>
];

    $c->response->body( $template );
}

Now we are ready to visit the URL http://localhost:3000/add… huh?

Yeah, page not found.

Let us take a look at the output of our server:

[info] *** Request 4 (0.028/s) [3055] [Sun Jun 21 14:41:35 2009] ***
[debug] "GET" request for "add" from "127.0.0.1"
[debug] Arguments are "add"
[info] Request took 0.004999s (200.040/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /default                                                   | 0.000192s |
| /end                                                       | 0.000275s |
'------------------------------------------------------------+-----------'

Looks like Catalyst server launched the default action. When it gets hit with the request, server tries to dispatch the URL into action, and if everything fails, the default action of the Root controller is used. It is done by checking actions’ attributes, like Path or Args. Both index and add have the empty Path attribute, and that’s why the add was not launched.

Now there are two basic options:

The attribute Local will make the action (yes, you guessed it!) local to the current controller, thus making the action reachable. If we make the action local, it will be bound to the containing controller. If the action dump of the controller Core is local, after renaming the controller to Kernel the URL /core/dump will become unreachable, and the action will be available under /kernel/dump. After making the add action local, the output of starting Catalyst server will look like this:

[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private              | Class                                | Method       |
+----------------------+--------------------------------------+--------------+
| /add                 | helloworld::Controller::Root         | add          |
| /default             | helloworld::Controller::Root         | default      |
| /end                 | helloworld::Controller::Root         | end          |
| /index               | helloworld::Controller::Root         | index        |
'----------------------+--------------------------------------+--------------'

[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path                                | Private                              |
+-------------------------------------+--------------------------------------+
| /                                   | /default                             |
| /                                   | /index                               |
| /add                                | /add                                 |
'-------------------------------------+--------------------------------------'

The attribute Path has to contain (yes, you guessed it again!) the path under which the action will be available. Now nobody can stop us from making the add action reachable under some really fancy URL:

sub add :Path('/now/you/can/add') :Args(0) {

and then enjoying the following output:

[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private              | Class                                | Method       |
+----------------------+--------------------------------------+--------------+
| /add                 | helloworld::Controller::Root         | add          |
| /default             | helloworld::Controller::Root         | default      |
| /end                 | helloworld::Controller::Root         | end          |
| /index               | helloworld::Controller::Root         | index        |
'----------------------+--------------------------------------+--------------'

[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path                                | Private                              |
+-------------------------------------+--------------------------------------+
| /                                   | /default                             |
| /                                   | /index                               |
| /now/you/can/add                    | /add                                 |
'-------------------------------------+--------------------------------------'

There is really no distinction in the Private actions section, but when it comes to Path actions, the difference is quite easy to spot. In this example I decided to use the Local attribute.

Now the action should at last work. After filling the textbox we can hit the button, seeing the server coping with the data we provided:

[info] *** Request 2 (0.667/s) [3107] [Sun Jun 21 15:16:38 2009] ***
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter                           | Value                                |
+-------------------------------------+--------------------------------------+
| addit                               | Add it!                              |
| newitem                             | magic                                |
'-------------------------------------+--------------------------------------'
[debug] "GET" request for "add" from "127.0.0.1"
[debug] Path is "add"
[info] Request took 0.004661s (214.546/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /add                                                       | 0.000187s |
| /end                                                       | 0.000324s |
'------------------------------------------------------------+-----------'

which is, of course, impressive. But it doesn’t add anything at all! If we go to the “main page”, we will see the same list. That should be changed, so the add action can modify the list of items. To achieve this, first we have to make the list available for the action, making it global:

my @items = ( 'beer', 'bacon', 'pierogi' );

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    my $list = '';

    foreach my $item ( @items ) {
        $list .= "<li>$item</li>\n";
    }

    my $template = qq[
<html>
    <head>
        <title>My items</title>
    </head>
    <body>
        <ul>
        $list
        </ul>
        <p>Only \$5!</p>
    </body>
</html>
];

    $c->response->body( $template );
}

Now the second action can modify the list:

sub add :Local :Args(0) {
    my ( $self, $c ) = @_;

    if ( $c->req->params->{newitem} ) {
        push @items, $c->req->params->{newitem};
    }

    my $template = qq[
<html>
    <head>
        <title>Add new item</title>
    </head>
    <body>
        <div align="center">
            <h3>Add something!</h3>
            <form method="get">
                <input type="text" size="24" id="newitem" name="newitem">
                <input type="submit" id="addit" name="addit" value="Add it!">
            </form>
        </div>
    </body>
</html>
];

    $c->response->body( $template );
}

The $c variable is called “context”. Usually if you want to get something from Catalyst, first try to ask the context object if it has seen it. Inside of the context one can find the request and response object (with their req and res aliases for those of us who lost the war with laziness). Request may contain some parameters, called params; and those parameters are accessed by their respective names. This may be used to write the code like this:

sub add :Local :Args(0) {
    my ( $self, $c ) = @_;

    my $optional;

    if ( $c->req->params->{newitem} ) {
        push @items, $c->req->params->{newitem};

        if ( $c->req->params->{addit} eq 'Add it!' ) {
            $optional = '<h4>Thank you for filling the form!</h4>';
        }
    }

    my $template = qq[
<html>
    <head>
        <title>Add new item</title>
    </head>
    <body>
        <div align="center">
            <h3>Add something!</h3>
            <form method="get">
                <input type="text" size="24" id="newitem" name="newitem">
                <input type="submit" id="addit" name="addit" value="Add it!">
            </form>
            $optional
        </div>
    </body>
</html>
];

    $c->response->body( $template );
}

Next time I will dwell a little bit on the second attribute – Args; that in turn will be useful for creating the API, so you would be able to build Facebook, in case it wasn’t built already.

Posted in Catalyst for intimidated | Tagged: , , , , , , , | 1 Comment »