Выпуск 3. Май 2013

Введение в Perl XS | Содержание | Обзор CPAN за апрель 2013 г.

Введение в разработку web-приложений на PSGI/Plack. Часть 2.

Продолжение статьи о PSGI/Plack. Рассмотрены более подробно Plack::Builder, а также Plack::Middleware.

В прошлой статье мы рассмотрели спецификацию PSGI, как она появилась, почему ей стоит пользоваться. Рассмотрели Plack — реализацию PSGI, основные его компоненты и написали простейшее API, которое выполняло поставленные перед ним задачи, вскользь рассмотрели основные PSGI сервера.

Во второй части статьи мы рассмотрим следующие моменты:

  • Plack::Builder — мощный маршрутизатор и не только.
  • Plack::Middleware — расширяем наши возможности при помощи «прослоек».

Мы по-прежнему используем Starman, который является preforking сервером (использует модель предварительно запущенных процессов).

Более пристальный взгляд на Plack::Builder

В предыдущей статье мы вкратце рассмотрели Plack::Builder. Теперь пришло время рассмотреть его более подробно. Решение рассматривать Plack::Builder вместе с Plack::Middleware весьма логично, потому что они очень тесно взаимосвязаны. Рассматривая в разных статьях эти два компонента, обе статьи содержали бы перекрестные ссылки друг на друга, что не очень удобно в формате журнала.

Базовая конструкция Plack::Builder выглядит так:

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

Эта конструкция указывает нам на то, что по адресу /foo будет располагаться PSGI-приложение ($bar). То, что мы обернули в builder должно быть обязательно ссылкой на функцию, иначе можем получить ошибку следующего вида:

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.

Маршруты могут быть вложенными, например:

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

Эта запись означает, что по адресу /foo будет располагаться приложение $foo, по адресу /foo/bar приложение $bar, а по адресу /foo/baz приложение $baz соответственно.

Однако, никто не мешает записать предыдущую запись в следующем виде:

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

Обе записи эквивалентны и выполняют одну и ту же задачу, но первая выглядит проще и понятнее. Plack::Builder можно использовать в объектно-ориентированном стиле, но лично мне удобнее использовать его в процедурном виде. Применение Plack::Builder в объектно-ориентированном виде выглядит так:

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

Эта запись эквивалентна:

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

Какой из способов использовать — дело сугубо индивидуальное. Мы вернемся к рассмотрению Plack::Builder после ознакомления с Plack::Middleware.

Plack::Middleware

Plack::Middleware — это базовый класс для написания, как говорит нам CPAN, «простых в использовании PSGI-прослоек». Для чего это нужно? Рассмотрим на примере реализации некоего API.

Представим, что код нашего приложения выглядит так:

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 };
}

Это приложение отлично работает, но теперь представим, что вдруг понадобилось принимать данные только в том случае, если они передаются методом POST.

Тривиальное решение — привести наше приложение к следующему виду:

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();
};

Понадобилось всего лишь 4 строки, чтобы решить проблему. А теперь представим, что понадобилось сделать еще одно приложение, которое тоже должно принимать данные отправленные только методом POST. Что будем делать? Писать в каждом это условие? Это не вариант по нескольким причинам:

  • Увеличивается объем кода, а как следствие его энтропия (простое лучше, чем сложное).
  • Больше вероятность сделать ошибку (человеческий фактор).
  • Если мы передадим проект другому программисту, он может забыть и сделать что-то не так (человеческий фактор).

Итак, сформулируем проблему. Мы не можем сделать так, чтобы все наши приложения одновременно приобрели определенные свойства не изменяя их код. Или можем?

Механизм Middleware отлично подходит для предоставления сквозного функционала всему приложению. Стоит, конечно, чувствовать меру и добавлять только действительно необходимый всей программе код.

Для того, чтобы построить свое Middleware (свою прослойку, другими словами), необходимо добиться исполнения следующих условий:

  • Находиться в пакете Plack::Middleware::MYCOOLMIDDLEWARE, где MYCOOLMIDDLEWARE название вашего Middleware.
  • Расширять базовый класс Plack::Middleware (use parent qw/Plack::Middleware/;).
  • Реализовывать метод (функцию) call.

Итак, реализуем простейшее 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);
}

Рассмотрим подробнее то, что получилось. Есть код, который находится в пакете Plack::Middleware (1 пункт), который наследует базовый класс Plack::Middleware (2 пункт), реализовывает метод call (3 пункт).

Представленная реализация call делает следующее:

  • Принимает в качестве параметров экземпляр Plack::Middleware и env (my ($self, $env) = @_;).
  • Создает запрос, который принимает приложение (создание аналогично тому, которое использовалась в предыдущих примерах).
  • Проверяет, не является ли метод запроса POST, если является, то Middleware пропускает обработку запроса дальше.

Рассмотрим, что происходит, если метод запроса не является POST.

Если метод не является POST создается новый объект Plack::Response и сразу же возвращается, не вызывая приложение.

Вообще функция call в Middleware может делать ровно 2 действия. Это:

  • Обработка env ПЕРЕД выполнением приложения.
  • Обработка результата ПОСЛЕ выполнения приложения.

Это будет проиллюстрировано в конце статьи, когда мы будем подводить итоги и разбираться в нюансах.

Совместное использование Plack::Middleware и Plack::Builder

Есть готовая прослойка Plack::Middleware::PostOnly, у нас есть PSGI прилжение, у нас есть проблема. Напоминаю, проблема выглядит так: “На данный момент мы не можем глобально влиять на поведение приложений”. Теперь можем. Рассмотрим самый важный момент Plack::Builder — ключевое слово enable.

Ключевое слово enable позволяет подключать Plack::Middleware к приложению. Делается это следующим образом:

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

Это очень простой и очень мощный механизм одновременно. Объединим весь код в одном месте и посмотрим на результат.

PSGI приложение:

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);
}

Приложение запускается следующей командой:

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

В коде использовалось enable "PostOnly" потому, что Plack::Builder автоматически подставляет к имени пакета Plack::Middleware. Пишется enable "PostOnly", имеется в виду enable "Plack::Middleware::PostOnly" (указать полный путь к своему классу можно используя в качестве префикса +, например, enable "+MyApp::Middleware::PostOnly"; – прим. редактора).

Теперь, если обратиться по адресу http://localhost:8080/ при помощи метода GET, то получим сообщение о том, что Method not allowed с кодом ответа 405, тогда как при обращении методом POST все будет нормально.

Не зря в коде приложения присутствует строка warn "WORKS". Она подтверждает отсутствие выполнения приложения, если метод не является POST. Попробуйте отправить GET, вы не увидите этого сообщения в STDERR starman.

У PSGI-серверов есть еще довольно много интересных особенностей поведения, они обязательно будут рассмотрены в следующих статьях.

Рассмотрим еще несколько полезных моментов Plack::Middleware, а именно:

  • Обработка результатов ПОСЛЕ выполнения приложения.
  • Передача параметров в Middleware.

Допустим, есть два PSGI-приложения и необходимо сделать так, чтобы одно работало через POST, а другое только через GET. Можно решить проблему в лоб, написав еще одно Middleware, которое будет отвечать только на метод GET, например, так:

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);
}

Задача решена, однако остается много дублирования.

Решение этой проблемы как нельзя лучше поможет разобраться со следующими вещами:

  • Механизмы передачи переменных в Middleware.
  • Подключение Middleware для приложений индивидуально.

Решение проблемы следующее: передавать желаемый метод в качестве переменной. Вернемся к рассмотрению enable из Plack::Builder. Оказывается, enable умеет принимать переменные. Выглядит это следующим образом:

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

В самом Middleware к этим переменным можно добраться напрямую через $self. Например, для того чтобы получить значение, переданное переменной one, необходимо обратиться к $self->{one} в коде Middleware. Продолжаем изменять PostOnly.

Пример:

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);
}

Перезапускаем starman, делаем запрос на localhost:8080, в STDERR видим следующее:

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

Так передаются переменные в Plack::Middleware.

Используя данный механизм, напишем Middleware, которое теперь будет называться 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;

Теперь Middleware умеет отвечать только на переданный в параметрах метод запроса. Немного изменившееся подключение выглядит так:

my $main_app = builder {
    enable "Only", method => 'POST';
    mount "/" => builder { $api_app };
};

В данном случае приложение будет исполняться только в том случае, если метод запроса был POST.

Рассмотрим обработку результатов ПОСЛЕ выполнения приложения. Допустим, необходимо чтобы в случае, если метод разрешен, к телу ответа добавлялось слово “ALLOWED”.

То есть, если приложение должно отдавать ok, оно отдаст ok ALLOWED, если, конечно, запрос будет выполнен с допустимым методом.

Приведем Only.pm к следующему виду:

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) возвращает ссылку на массив из трех элементов (PSGI-спецификация), тело которого модифицируется и отдается в качестве ответа.

Убедится, что это все работает и работает так как надо, можно передав разрешенным методом переменную string=data и string=data1. В первом случае, если метод разрешен, ответ будет выглядеть “okALLOWED”, во втором “not okALLOWED”.

И в заключение рассмотрим, как именно можно объединить все вышеперечисленное в одно Plack-приложение. Возвращаемся к первоначальной задаче. Необходимо разработать простейшее API, которое принимает переменную string и если string=data ответить ok, иначе not ok, а также соблюдать следующие правила:

  • При обращении по адресу / отвечать на любой метод.
  • При обращении по адресу /post отвечать только на метод POST.
  • При обращении по адресу /get отвечать только на метод GET.

На самом деле, понадобится ровно одно приложение, которое написано — $api_app и немного модифицированный builder.

В результате, используя все вышеописанное, должно получиться приложение следующего вида:

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;
        };
    };
};

Таким образом, подключение Middleware работает во вложенных маршрутах Plack::Builder. Стоит обратить внимание на простоту и логичность кода.

Отложенный ответ будет рассмотрен в одной из статей посвященных асинхронным серверам (Twiggy, Corona, Feersum).

Дмитрий Шаматрин


Введение в Perl XS | Содержание | Обзор CPAN за апрель 2013 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье