With the permission of the author and chief editor of the magazine PragmaticPerl.com, I continue to publish a series of articles.
The original article is here.Continuing the article on PSGI / Plack. Plack :: Builder, as well as Plack :: Middleware are considered in more detail.In the last article, we looked at the PSGI specification, how it came out, why it should be used. We considered Plack - the implementation of PSGI, its main components and wrote the simplest API that performed the tasks assigned to it, briefly examined the main PSGI server.
In the second part of the article we will consider the following points:
')
- Plack :: Builder is a powerful router and not only.
- Plack :: Middleware - we expand 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 the previous article, we briefly reviewed Plack :: Builder. Now it's time to look at it in more detail. The decision to consider Plack :: Builder with Plack :: Middleware is quite 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 a journal format.
The basic Plack :: Builder construction 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 the builder must be a reference to the function, otherwise we may get an error of the following form:
Can not use the 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 $ foo application will be located at the / foo address, the $ bar application at the / foo / bar address, and the $ baz application at the / foo / baz address, respectively.
However, no one bothers to write the previous entry 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 clearer. Plack :: Builder can be used in object-oriented style, but I personally find it more convenient to use it in a procedural form. Using 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 of the ways to use is a purely individual matter. We will return to reviewing Plack :: Builder after becoming familiar with 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 the example of the implementation of a certain 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 all you need to do is receive data only if it is transmitted using the POST method.
A 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. And now let's 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 of 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 may forget and do something wrong (human factor).
So, we formulate the problem. We cannot make all our applications simultaneously acquire certain properties without changing their code. Or can we?
The Middleware mechanism is great for providing end-to-end functionality to the entire application. It is, of course, necessary to feel the measure and add only the code really necessary for 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 the method (function) call.
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's take a closer look at what happened. There is a code that is in the Plack :: Middleware package (1 point), which inherits the base class Plack :: Middleware (2 point), implements the call method (3 point).
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 is similar to that used in the previous examples).
- Checks if the POST request method is not, if it is, then Middleware skips processing the request 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 immediately returned without invoking the application.
In general, the call function in Middleware can do exactly 2 actions. It:
- Processing env BEFORE executing an application.
- Processing result AFTER the application is executed.
This will be illustrated at the end of the article, when we summarize and understand the nuances.
Sharing Plack :: Middleware and Plack :: Builder
There is a ready Plack :: Middleware :: PostOnly layer, 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 an 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 by the following command:
/usr/bin/starman --port 8080 app.psgi
The code used to enable “PostOnly” because Plack :: Builder automatically substitutes for the name of the Plack :: Middleware package. It is written to enable “PostOnly”, meaning “Plack :: Middleware :: PostOnly” (refer to the full path to your class using the prefix +, for example, enable "+ MyApp :: Middleware :: PostOnly"; - approx. editor).
Now, if you contact the address
localhost : 8080 / with the help of the GET method, you will receive a message that Method not allowed with the response code 405, whereas if you use the POST method, everything will be fine.
Not for nothing in the application code there is a warn string "WORKS". It confirms the lack of execution of the application, if the method is not POST. Try sending GET, you will not see this message in STDERR starman.
PSGI servers still have quite a few interesting features of behavior, they will definitely be discussed in the following articles.
Let us consider some more useful points of Plack :: Middleware, namely:
- Processing of the results AFTER the execution of the application.
- 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 in the forehead by writing another Middleware, which 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, however, a lot of duplication remains.
The solution to this problem is the best way to deal with the following things:
- Mechanisms for transferring variables to Middleware.
- Connecting Middleware for applications individually.
The solution to the problem is to pass the desired method as a variable. Let us return to the consideration of 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 via $ self. For example, in order to get the value passed to the one variable, you need to refer to $ self -> {one} in the Middleware code. We continue 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); }
Restart the 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.
This is how variables are passed 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 different 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 that if the method is allowed, the word “ALLOWED” should be added to the response body.
That is, if the application should give ok, it will return ok ALLOWED, if, 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), whose body is modified and given as an answer.
You can make sure that everything works and works as it should, passing the variable string = data and string = data1 by the allowed method. In the first case, if the method is allowed, the answer will look “okALLOWED”, in the second “not okALLOWED”.
And in conclusion, let us consider exactly how you can combine all of the above in one Plack-application. We return to the original problem. It is necessary to develop a simple API that accepts the string variable and if string = data answer ok, otherwise not ok, and also observe the following rules:
When contacting / responding to any method.
When contacting the / post address, respond only to the POST method.
When contacting / get, respond only to the GET method.
In fact, you 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, the Middleware connection works in nested Plack :: Builder routes. It is worth paying attention to the simplicity and logic of the code.
The delayed response will be discussed in one of the articles devoted to asynchronous servers (Twiggy, Corona, Feersum).