Introduction to web application development on PSGI / Plack. Part 2

  • Tutorial
With the permission of the author and editor-in-chief of PragmaticPerl.com, I continue to publish a series of articles.
Original article here.
Continuation of the article on PSGI / Plack. Considered in more detail Plack :: Builder, as well as Plack :: Middleware.

In the last article, we looked at the PSGI specification, how it appeared, why it should be used. We examined Plack - the implementation of PSGI, its main components and wrote the simplest API that performed the tasks assigned to it, casually examined the main PSGI servers.

In the second part of the article we will consider the following points:

  • Plack :: Builder is a powerful router and more.
  • Plack :: Middleware - expanding our capabilities with the help of "layers".


We still use Starman, which is a preforking server (uses a model of pre-running processes).

A closer look at Plack :: Builder


In a previous article, we briefly reviewed Plack :: Builder. Now it's time to consider it in more detail. The decision to consider Plack :: Builder together with Plack :: Middleware is very logical, because they are very closely interconnected. Considering these two components in different articles, both articles would contain cross-references to each other, which is not very convenient in the journal format.

The basic construction of Plack :: Builder looks like this:

builder {
    mount '/foo' => builder { $bar };
}


This construction tells us that the PSGI application ($ bar) will be located at / foo. What we wrapped in builder must be a function reference, otherwise we can get an error of the following form:

Can't use string (“stupid string”) as a subroutine ref while “strict refs” in use at / usr / local / share /perl/5.14.2/Plack/App/URLMap.pm line 71.


Routes can be nested, for example:

builder {
    mount '/foo' => builder {
        mount '/bar' => builder { $bar; };
        mount '/baz' => builder { $baz; };
        mount '/'    => builder { $foo; };
    };
};


This entry means that the application $ foo will be located at the address / foo, the application $ bar at the address / foo / bar, and the application $ baz at the address / foo / baz, respectively.

However, no one bothers to record the previous record in the following form:

builder {
    mount '/foo/bar' => builder { $bar };
    mount '/foo/baz' => builder { $baz };
    mount '/foo/'    => builder { $foo };
};


Both entries are equivalent and perform the same task, but the first one looks simpler and more understandable. Plack :: Builder can be used in an object-oriented style, but I personally prefer to use it in a procedural form. An application of Plack :: Builder in an object-oriented form looks like this:

my $builder = Plack::Builder->new;
$builder->mount('/foo' => $foo_app);
$builder->mount('/'    => $root_app);
$builder->to_app;


This entry is equivalent to:

builder {
    mount '/foo' => builder { $app; };
    mount '/'    => builder { $app2; };
};


Which way to use is a purely individual matter. We will return to Plack :: Builder after reviewing Plack :: Middleware.

Plack :: Middleware


Plack :: Middleware is the base class for writing, as CPAN tells us, "easy-to-use PSGI layers." What is it for? Consider an example implementation of an API.

Imagine that our application code looks like this:

my $api_app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    my $params = $req->parameters();
    if ($params->{string} && $params->{string} eq 'data') {
        $res->body('ok');
    }
    else {
        $res->body('not ok');
    }
    return $res->finalize();
};
my $main_app = builder {
    mount "/" => builder { $api_app };
}

This application works fine, but now imagine that you suddenly needed to receive data only if it was transmitted using the POST method.

The trivial solution is to bring our application to the following form:

my $api_app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    my $params = $req->parameters();
    if ($req->method() ne 'POST') {
        $res->status(403);
        $res->body('Method not allowed');
        return $res->finalize();
    }
    if ($params->{string} && $params->{string} eq 'data') {
        $res->body('ok');
    }
    else {
        $res->body('not ok');
    }
    return $res->finalize();
};


It took only 4 lines to solve the problem. Now imagine that it was necessary to make another application, which should also accept data sent only by the POST method. What do we do? Write in each this condition? This is not an option for several reasons:

  • The amount of code increases, and as a result, its entropy (simple is better than complex).
  • More likely to make a mistake (human factor).
  • If we transfer the project to another programmer, he can forget and do something wrong (human factor).

So, we formulate the problem. We cannot make sure that all our applications simultaneously acquire certain properties without changing their code. Or can we?

The Middleware engine is great for providing end-to-end functionality to the entire application. It’s worth, of course, to feel the measure and add only the code really needed by the whole program.

In order to build your Middleware (your layer, in other words), you must achieve the following conditions:

  • Be in the package Plack :: Middleware :: MYCOOLMIDDLEWARE, where MYCOOLMIDDLEWARE is the name of your Middleware.
  • Extend the base class Plack :: Middleware (use parent qw / Plack :: Middleware /;).
  • Implement a call method (function).


So, we implement the simplest Middleware, considering all of the above:

package Plack::Middleware::PostOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    if ($req->method() ne 'POST') {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    return $self->app->($env);
}

Let us consider in more detail what happened. There is code that is in the package Plack :: Middleware (1 point), which inherits the base class Plack :: Middleware (2 points), implements the call method (3 points).

The presented call implementation does the following:

  • Takes an instance of Plack :: Middleware and env (my ($ self, $ env) = @_;) as parameters.
  • Creates a request that the application accepts (creation similar to that used in previous examples).
  • Checks if the POST request method is if it is, then Middleware skips the request processing further.


Consider what happens if the request method is not POST.

If the method is not POST, a new Plack :: Response object is created and returned immediately without calling the application.

In general, the call function in Middleware can do exactly 2 actions. It:

  • Env processing BEFORE running the application.
  • Processing the result AFTER the application.


This will be illustrated at the end of the article, when we will summarize and understand the nuances.

Sharing Plack :: Middleware and Plack :: Builder


There is a ready-made layer of Plack :: Middleware :: PostOnly, we have a PSGI application, we have a problem. I remind you that the problem looks like this: “At the moment, we cannot globally influence the behavior of applications.” Now we can. Consider the most important point of Plack :: Builder - the enable keyword.

The enable keyword allows you to connect Plack :: Middleware to the application. This is done as follows:

my $main_app = builder {
    enable "PostOnly";
    mount "/" => builder { $api_app; };
}

This is a very simple and very powerful mechanism at the same time. Combine all the code in one place and look at the result.

PSGI application:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
use Plack::Middleware::PostOnly;
my $api_app = sub {
    my $env = shift;
    warn 'WORKS';
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    my $params = $req->parameters();
    if ($params->{string} && $params->{string} eq 'data') {
        $res->body('ok');
    }
    else {
        $res->body('not ok');
    }
    return $res->finalize();
};
my $main_app = builder {
    enable "PostOnly";
    mount "/" => builder { $api_app };
}

Middleware:

package Plack::Middleware::PostOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    if ($req->method() ne 'POST') {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    return $self->app->($env);
}

The application is launched with the following command:

/usr/bin/starman --port 8080 app.psgi


In the code, enable “PostOnly” was used because Plack :: Builder automatically substitutes Plack :: Middleware for the package name. It says enable "PostOnly", meaning enable "Plack :: Middleware :: PostOnly" (you can specify the full path to your class using the + prefix, for example, enable "+ MyApp :: Middleware :: PostOnly"; - approx. editor).

Now, if you go to localhost : 8080 / using the GET method, you will get a message saying that Method not allowed with a response code of 405, while if you use the POST method, everything will be fine.

It is not in vain that the warning line “WORKS” is present in the application code. It confirms the lack of application execution if the method is not POST. Try sending a GET, you will not see this message in STDERR starman.

PSGI-servers have quite a few interesting behavior features, they will be definitely considered in the following articles.

Let's look at a few more useful points of Plack :: Middleware, namely:

  • Processing results AFTER application execution.
  • Passing parameters to Middleware.


Suppose there are two PSGI applications and you need to make sure that one works through POST, and the other only through GET. You can solve the problem head on by writing another Middleware that will respond only to the GET method, for example, like this:

package Plack::Middleware::GetOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    if ($req->method() ne 'GET') {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    return $self->app->($env);
}


The problem is solved, but a lot of duplication remains.

Solving this problem will help you deal with the following things:
  • Mechanisms for passing variables to Middleware.
  • Connecting Middleware for applications individually.


The solution to the problem is as follows: pass the desired method as a variable. Let's get back to considering enable from Plack :: Builder. It turns out that enable can accept variables. It looks like this:

my $main_app = builder {
    enable "Foo", one => 'two', three => 'four';
    mount "/" => builder { $api_app };
}

In Middleware itself, these variables can be accessed directly through $ self. For example, in order to get the value passed to the variable one, you need to refer to $ self -> {one} in the Middleware code. Continuing to change PostOnly.

Example:

package Plack::Middleware::GetOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    warn $self->{one} if $self->{one};
    if ($req->method() ne 'GET') {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    return $self->app->($env);
}

We restart starman, make a request to localhost: 8080, in STDERR we see the following:

two at /home/noxx/perl/lib/Plack/Middleware/PostOnly.pm line 12.

So variables are transferred to Plack :: Middleware.

Using this mechanism, we write Middleware, which will now be called Only.

package Plack::Middleware::Only;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    my $method = $self->{method};
    $method ||= 'ANY';
    if ($method ne 'ANY' && $req->method() ne $method) {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    return $self->app->($env);
}
1;

Now Middleware can only respond to the request method passed in the parameters. A slightly changed connection looks like this:
my $main_app = builder {
    enable "Only", method => 'POST';
    mount "/" => builder { $api_app };
};

In this case, the application will be executed only if the request method was POST.

Consider processing results AFTER application execution. Suppose, if the method is allowed, the word “ALLOWED” is added to the response body.

That is, if the application should give ok, it will give ok ALLOWED, unless, of course, the request is executed with a valid method.

Let's bring Only.pm to the following form:
package Plack::Middleware::Only;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
    my ($self, $env) = @_;
    my $req = Plack::Request->new($env);
    my $method = $self->{method};
    $method ||= 'ANY';
    if ($method ne 'ANY' && $req->method() ne $method) {
        my $new_res = $req->new_response(405);
        $new_res->body('Method not allowed');
        return $new_res->finalize();
    }
    my $plack_res = $self->app->($env);
    $plack_res->[2]->[0] .= 'ALLOWED';
    return $plack_res;
}
1;

$ self-> app -> ($ env) returns a reference to an array of three elements (PSGI specification), the body of which is modified and given as an answer.

Make sure that this all works and works as it should, by passing the string = data and string = data1 variables by the permitted method. In the first case, if the method is enabled, the answer will look “okALLOWED”, in the second “not okALLOWED”.

In conclusion, we will consider how exactly it is possible to combine all of the above into one Plack application. We return to the original task. It is necessary to develop a simple API that accepts the variable string and if string = data answer ok, otherwise not ok, and also observe the following rules:

When contacting the address / answer any method.
When accessing the address / post, respond only to the POST method.
When accessing the / get address, respond only to the GET method.
In fact, you will need exactly one application that is written - $ api_app and a slightly modified builder.

As a result, using all of the above, you should get an application of the following form:
use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
use Plack::Middleware::PostOnly;
use Plack::Middleware::Only;
my $api_app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    my $params = $req->parameters();
    warn 'RUN!';
    if ($params->{string} && $params->{string} eq 'data') {
        $res->body('ok');
    }
    else {
        $res->body('not ok');
    }
    return $res->finalize();
};
my $main_app = builder {
    mount "/" => builder {
        mount "/post" => builder {
            enable "Only", method => 'POST';
            $api_app;
        };
        mount "/get" => builder {
            enable "Only", method => 'GET';
            $api_app;
        };
        mount "/" => builder {
            $api_app;
        };
    };
};

Thus, Middleware connectivity works in nested Plack :: Builder routes. It is worth paying attention to the simplicity and consistency of the code.

The postponed answer will be considered in one of the articles devoted to asynchronous servers (Twiggy, Corona, Feersum).

Also popular now: