Данная страница документация не завершена.
Актуально для версии 1.22.00.
Все действия будут производиться в папке модуля, например, Abills/modules/Portal.
За пример взят модуль Portal.
Старт
Для начала, в корневой папке модуля файл Api.pm, с таким начальным содержанием:
# Мы объявляем package с названием "*модуль*::Api". package Portal::Api; =head1 NAME Portal Api =cut use strict; use warnings FATAL => 'all'; # Импортируем сообщения для ошибок use Control::Errors; my Control::Errors $Errors; #********************************************************** =head2 new($db, $conf, $admin, $lang, $debug, $type) =cut #********************************************************** # Создаём конструктор sub new { my ($class, $db, $admin, $conf, $lang, $debug, $type) = @_; my $self = { db => $db, admin => $admin, conf => $conf, lang => $lang, debug => $debug }; bless($self, $class); $self->{routes_list} = (); # Определяем, для чего роутер вызвал наш модуль API # Соответственно, ли это USER API или ADMIN API # И записываем routes_list if ($type eq 'user') { $self->{routes_list} = $self->user_routes(); } elsif ($type eq 'admin') { $self->{routes_list} = $self->admin_routes(); } $Errors = Control::Errors->new($self->{db}, $self->{admin}, $self->{conf}, # Обязательно обозначить что это за модуль, # чтобы система могла подгрузить сообщения для ошибок со словаря модуля за потребности { lang => $lang, module => 'Portal' } ); # Сохраняем словарь ошибок в объект, он нам потом будет нужен $self->{Errors} = $Errors; return $self; } #********************************************************** =head2 admin_routes() - Returns available ADMIN API paths =cut #********************************************************** sub admin_routes { # Здесь нужно возвращать специальный массив ADMIN API return []; } #********************************************************** =head2 user_routes() - Returns available USER API paths =cut #********************************************************** sub user_routes { # Здесь нужно возвращать специальный массив USER API return []; } 1;
Словарь ошибок
Документация по словарю ошибок
Важная часть - позволяет систематизировать ошибки и избежать разночтений ошибок во время написания кода.
Создаем в корневой папке модуля файл Errors.pm
Он должен выглядеть приблизительно так:
# Пэкэдж должен ОБЯЗАТЕЛЬНО иметь название *модуль*::Errors # Это позволяет библиотеке словаря ошибок его же найти package Portal::Errors; =head1 NAME Portal::Errors # Крайне рекомендуем указывать в pod какой префикс ошибок у модуля. # Это просто поможет в поддержке, понимать какой префикс у какого модуля. IDS: 144* =cut use strict; use warnings FATAL => 'all'; #********************************************************** =head2 errors() - errors list =cut #********************************************************** # Создаём функцию словаря ошибок с анонимным хэшэм. # В будущем он будет заполнен парами *errno* => *errstr* sub errors { return { # Семизначный код ошибки, с приставкой 144 (модуль Portal) => ключ в словаре ошибки # Примеры: 1440001 => 'ERR_PORTAL_NO_SENDER', 1440002 => 'ERR_PORTAL_NO_ARTICLE', }; } 1;
И не забудьте заполнить lng_english.pl
# Заполняем ключи ошибок, можем даже вставлять переменные $lang{ERR_PORTAL_NO_SENDER} = 'No sender with id %ID%'; $lang{ERR_PORTAL_NO_ARTICLE} = 'No article with id %ID%';
Валидатор
Документация по валидатору
Позволяет проверять полученный реквест от клиента по схеме.
Избегает вызова пути с неправильными параметрами, что повышает устойчивость системы и возможных ошибок.
Также позволяет чётко и подробно расписывать какие именно параметры отсутствуют или неправильные в целом.
Создаём корневой файл Validations.pm с таким примерным содержанием:
# Называем пэкэдж *модуль*::Validations package Portal::Validations; use strict; use warnings FATAL => 'all'; # Вызываем специальный модуль экспортера, для удобного экспортирования констант с описанием в IDE use Exporter; use parent 'Exporter'; # Записываем константы валидации в экспорт our @EXPORT = qw( POST_PORTAL_ARTICLES ); # Записываем константы валидации в экспорт our @EXPORT_OK = qw( POST_PORTAL_ARTICLES ); use constant { # Называем константу *МЕТОД*_ПУТЬ_ # А подробнее про валидатор можно узнать ссылкой выше. POST_PORTAL_ARTICLES => { TITLE => { required => 1, type => 'string', min_length => 5, max_length => 255 }, DATE => { required => 1, type => 'date' }, PORTAL_MENU_ID => { required => 1, type => 'string' }, SHORT_DESCRIPTION => { type => 'string', max_length => 600 }, CONTENT => { type => 'string', } }, }; 1;
ADMIN API
Создание роутов
Мы создали обязательное начало для API, теперь, если вам это нужно, создаём роуты для ADMIN API:
#********************************************************** =head2 admin_routes() - Returns available ADMIN API paths =cut #********************************************************** sub admin_routes { my $self = shift; return [ { # HTTP метод, GET, POST, PUT, PATCH, DELETE method => 'GET', # Абсолютный путь, за которым можно будет достучаться, например billing.url/api.cgi/portal/articles path => '/portal/articles/', # Указываем "контроллер" для API /portal/articles/* controller => 'Portal::Api::admin::Articles', # Даём ссылку на функцию-эндпойнт контроллера endpoint => \&Portal::Api::admin::Articles::get_portal_articles, credentials => [ # Определяем нужные параметры для авторизации. # ADMIN - API_KEY # ADMINSID - admin_sid по cookie (в том числе для api_call) 'ADMIN', 'ADMINSID' ] }, ] }
Мы создали свой первый роут, но нам ещё нужно создать для его base первый контроллер.
Вы, конечно, можете писать функцию сразу же в этом хэндлере, но мы не рекомендуем так делать, поскольку в будущем вам станет неудобно это поддерживать.
Поскольку практически каждая функция администратора должна иметь CRUD-составляющую, то вот как это выглядит для /portal/articles
return [ # Создать статью { method => 'POST', path => '/portal/articles/', # Параметры до валидатора params => POST_PORTAL_ARTICLES, controller => 'Portal::Api::admin::Articles', endpoint => \&Portal::Api::admin::Articles::post_portal_articles, credentials => [ 'ADMIN', 'ADMINSID' ] }, # Получить статьи { method => 'GET', path => '/portal/articles/', controller => 'Portal::Api::admin::Articles', endpoint => \&Portal::Api::admin::Articles::get_portal_articles, credentials => [ 'ADMIN', 'ADMINSID' ] }, # Получить конкретную статью { method => 'GET', path => '/portal/articles/:id/', controller => 'Portal::Api::admin::Articles', endpoint => \&Portal::Api::admin::Articles::get_portal_articles_id, credentials => [ 'ADMIN', 'ADMINSID' ] }, # Изменить конкретную статью { method => 'PUT', path => '/portal/articles/:id/', controller => 'Portal::Api::admin::Articles', endpoint => \&Portal::Api::admin::Articles::put_portal_articles_id, credentials => [ 'ADMIN', 'ADMINSID' ] }, # Удалить конкретную статью { method => 'DELETE', path => '/portal/articles/:id/', controller => 'Portal::Api::admin::Articles', endpoint => \&Portal::Api::admin::Articles::delete_portal_articles_id, credentials => [ 'ADMIN', 'ADMINSID' ] }, ]
Создание контроллера
Соответственно, как наши пути будут в /portal/articles/* и всё что с этим связано, и мы находимся в ADMIN API, то рекомендуем создать файл за такой схемой:Api/*тип API*/*Контроллер*.pm
например Api/admin/Articles.pm
Со следующим содержанием:
package Portal::Api::admin::Articles; =head1 NAME Portal articles manage # Рекомендуем в подах записывать к каким # группам эндпойнтов относится данный контроллер Endpoints: /portal/articles/* =cut use strict; use warnings FATAL => 'all'; use Control::Errors; # Импортируем объект Portal для работы с базой # он должен находиться в /usr/abills/Abills/mysql/Portal.pm use Portal; my Control::Errors $Errors; my Portal $Portal; my Portal::Misc::Attachments $Attachments; #********************************************************** =head2 new($db, $admin, $conf) =cut #********************************************************** sub new { my ($class, $db, $admin, $conf, $attr) = @_; my $self = { db => $db, admin => $admin, conf => $conf, attr => $attr }; bless($self, $class); $Portal = Portal->new($db, $admin, $conf); $Attachments = Portal::Misc::Attachments->new($self->{db}, $self->{admin}, $self->{conf}); # Определяем словарь ошибок, который нам пришёл выше $Errors = $self->{attr}->{Errors}; return $self; } # определяем здесь пути 1;
Определение роутов
Именно здесь вы можете определять базовую бизнес-логику.
Поскольку мы пытаемся в CRUD - определяем.
Create
#********************************************************** =head2 post_portal_articles($path_params, $query_params) Endpoint POST /portal/articles =cut #********************************************************** sub post_portal_articles { my $self = shift; my ($path_params, $query_params) = @_; if ($query_params->{PICTURE}) { my $picture_name = $Attachments->save_picture($query_params->{PICTURE}); $query_params->{PICTURE} = $picture_name; } my $permalink = $query_params->{PERMALINK} || _portal_generate_permalink($query_params->{TITLE}); return $Portal->portal_article_add({ %$query_params, PERMALINK => $permalink });; }
Read
#********************************************************** =head2 get_portal_articles($path_params, $query_params) # Всегда пишите в подах Endpoint *METHOD* *path* # Это позволит легче искать путь во время разработки. Endpoint GET /portal/articles =cut #********************************************************** sub get_portal_articles { my $self = shift; my ($path_params, $query_params) = @_; # Определяем системные параметры, сортировки, пагинации my %PARAMS = ( COLS_NAME => 1, PAGE_ROWS => $query_params->{PAGE_ROWS} ? $query_params->{PAGE_ROWS} : 25, SORT => $query_params->{SORT} ? $query_params->{SORT} : 1, PG => $query_params->{PG} ? $query_params->{PG} : 0, DESC => $query_params->{DESC}, ); # Даём возможность сортировки с помощью ?filename&file_size&file_type foreach my $param (keys %{$query_params}) { $query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0') ? $query_params->{$param} : '_SHOW'; } # Вызываем функцию для извлечения списка из базы, с нашими параметрами и которые определены вызовом # которые будут внутри обрабатываться search_former my $list = $Portal->portal_articles_list({ %$query_params, %PARAMS }); my @result = map { my $article_sublink = $_->{permalink} || $_->{id}; my $picture_link = $_->{picture} ? $self->_portal_picture_link($_->{picture}) : ''; $_->{url} = $self->_portal_news_link($article_sublink); $_->{picture} = $picture_link; $_ } @$list; # Настоятельная рекомендация: когда вы создаёте путь, который возвращает массив, то возвращайте # его с объектом с ключём list, а в total возвращайте общее число айтемов - это позволит работать пагинации # и опеределять дополнительные параметры. Будет легче поддерживать. return { list => \@result, total => $Portal->{TOTAL} }; } #********************************************************** =head2 get_portal_articles_id($path_params, $query_params) Endpoint GET /portal/articles/:id/ =cut #********************************************************** sub get_portal_articles_id { my $self = shift; my ($path_params, $query_params) = @_; foreach my $param (keys %{$query_params}) { $query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0') ? $query_params->{$param} : '_SHOW'; } $query_params->{COLS_NAME} = 1; my $list = $Portal->portal_articles_list({ ID => $path_params->{id}, %$query_params }); my @result = map { my $article_sublink = $_->{permalink} || $_->{id}; my $picture_link = $_->{picture} ? $self->_portal_picture_link($_->{picture}) : ''; $_->{url} = $self->_portal_news_link($article_sublink); $_->{picture} = $picture_link; $_ } @$list; return $result[0] || {}; }
Update
#********************************************************** =head2 put_portal_articles_id($path_params, $query_params) PUT /portal/articles/:id =cut #********************************************************** sub put_portal_articles_id { my $self = shift; my ($path_params, $query_params) = @_; if ($query_params->{PICTURE}) { my $picture_name = $Attachments->save_picture($query_params->{PICTURE}, $path_params->{id}); $query_params->{PICTURE} = $picture_name; } my $permalink = $query_params->{PERMALINK} || _portal_generate_permalink($query_params->{TITLE}); return $Portal->portal_article_change({ %$query_params, PERMALINK => $permalink }); }
Delete
#********************************************************** =head2 delete_portal_articles_id($path_params, $query_params) Endpoint DELETE /portal/articles/:id/ =cut #********************************************************** sub delete_portal_articles_id { my $self = shift; my ($path_params, $query_params) = @_; my $list = $Portal->portal_articles_list({ ID => $path_params->{id}, COLS_NAME => 1 }); if (!($list && scalar(@$list))) { # Не забудьте написать этот код ошибки в словаре! return $Errors->throw_error(1440002, { lang_vars => { ID => $path_params->{id} }}); } my $result = $Portal->portal_article_del({ ID => $path_params->{id} }); if (!$Portal->{errno}) { $Attachments->delete_attachment($path_params->{id}); } return $result; }
Написание OpenAPI
Обязательная составляющая, так как нужно разработчикам узнать, как с вашим API взаимодействовать.
У нас есть микрофреймворк с работой "сверху" над ним, чтобы учитывать нашу модульность.
Помним, что в рамках определения OpenAPI модуля есть:
- Файл Api/swagger/(admin|user)/paths.yaml - основа
- Папка Api/swagger/(admin|user)/paths - определения путей
- Папка Api/swagger/(admin|user)/schemas - определения схем
Создаём файл Abills/modules/Portal/Api/swagger/admin/paths.yaml
И определяем:
/portal/articles: $ref: "./paths/articles.yaml" /portal/articles/{ID}: $ref: "./paths/article.yaml"
Мы записываем базисы путей, и где они определяются.
USER API
Создание роутов
Если нужно USER API, заполняем и это:
#********************************************************** =head2 user_routes() - Returns available USER API paths =cut #********************************************************** sub user_routes { my $self = shift; return [ { method => 'GET', # Для USER API ОБЯЗАТЕЛЬНО начинаем абсолютный путь с /user/*. path => '/user/portal/menu/', # Подключаем "контроллер" для API /user/portal/* controller => 'Portal::Api::user::News', # Даём ссылку на функцию-эндпойнт контроллера endpoint => \&Portal::Api::user::News::get_user_portal_news, credentials => [ # Определяем нужные параметры для авторизации. # USER - авторизация по header, полученном с /user/login # USERSID - авторизация по cookie (в том числе для api_call) # PUBLIC - без авторизации # Тоесть, в данном случае путь может работать как и с авторизованными пользователями, так и нет. # Внутри хэндлера можно определять какой пользователь, об этом ниже. 'USER', 'USERSID', 'PUBLIC' ] }, ] }
Создание контроллера
Контроллер для USER API совсем ничем не отличается.
package Portal::Api::user::News; =head1 NAME User Portal # Рекомендуем в подах записывать к каким # группам эндпойнтов относится данный контроллер Endpoints: /user/portal/news* /user/portal/menu =cut use strict; use warnings FATAL => 'all'; use Control::Errors; # Импортируем объект Portal для работы с базой # он должен находиться в /usr/abills/Abills/mysql/Portal.pm use Portal; my Portal $Portal; my Control::Errors $Errors; #********************************************************** =head2 new($db, $admin, $conf) =cut #********************************************************** sub new { my ($class, $db, $admin, $conf, $attr) = @_; my $self = { db => $db, admin => $admin, conf => $conf, attr => $attr }; bless($self, $class); $Portal = Portal->new($db, $admin, $conf); # Определяем словарь ошибок, который нам пришёл выше $Errors = $self->{attr}->{Errors}; return $self; } # здесь определять пути 1;
Определение роутов
В целом, определение роутов для USER API ничем не отличается от ADMIN API, кроме одной важной детали - в $path_params при авторизации будет приходить uid.
Очень важная составляющая.
#********************************************************** =head2 get_user_portal_news($path_params, $query_params) Endpoint GET /user/portal/news =cut #********************************************************** sub get_user_portal_news { my $self = shift; my ($path_params, $query_params) = @_; # Не обязательно писать всю логику прямо внутри эндпоинта, как в примере с ADMIN API # Вы можете делить логику в бизнес-функции, для сокращения использования. # Но для простоты понимания, с самого начала лучше писать всё в эндпоинтах return $self->_portal_menu({ # Если пользователь авторизован - в $path_params->{uid} будет UID пользователя. # Если нет - поле будет пустое. UID => $path_params->{uid} || '', DOMAIN_ID => $query_params->{DOMAIN_ID}, PORTAL_MENU_ID => $query_params->{PORTAL_MENU_ID}, MAIN_PAGE => $query_params->{MAIN_PAGE}, LIST => 1 }); }
Практики
Поиск, сортировка, пагинация
Для правильной работы сортировки мы рекомендуем делать это не вручну, а с помощью search_former которые находятся внутри любого современного модуля.
Он выглядит приблизительно так, с точки зрения пути GET /portal/menus
#********************************************************** =head2 get_portal_menus($path_params, $query_params) Endpoint GET /portal/menus =cut #********************************************************** sub get_portal_menus { my $self = shift; my ($path_params, $query_params) = @_; # Проверяем, определяем стандартные параметры для вызова my %PARAMS = ( COLS_NAME => 1, PAGE_ROWS => $query_params->{PAGE_ROWS} ? $query_params->{PAGE_ROWS} : 25, SORT => $query_params->{SORT} ? $query_params->{SORT} : 1, PG => $query_params->{PG} ? $query_params->{PG} : 0, DESC => $query_params->{DESC}, ); foreach my $param (keys %{$query_params}) { $query_params->{$param} = ($query_params->{$param} || "$query_params->{$param}" eq '0') ? $query_params->{$param} : '_SHOW'; } my $list = $Portal->portal_menu_list({ ID => '_SHOW', NAME => '_SHOW', URL => '_SHOW', DATE => '_SHOW', STATUS => '_SHOW', # С помощью внутренней деструктуризации присваиваем %$query_params, # Оверрайдим параметры %PARAMS, }); return { list => $list, total => $Portal->{TOTAL} }; }
И смотрим что происходит под капотом у portal_menu_list
#********************************************************** =head2 function portal_menu_list() - get menu section list Arguments: $attr Returns: \@list - Examples: my $list = $Portal->portal_menu_list({COLS_NAME=>1}); =cut #********************************************************** sub portal_menu_list { my $self = shift; my ($attr) = @_; # Преопределяем параметры, если их нет my $SORT = ($attr->{SORT}) ? $attr->{SORT} : 1; my $DESC = ($attr->{DESC}) ? $attr->{DESC} : ''; my $PG = $attr->{PG} ? $attr->{PG} : 0; my $PAGE_ROWS = $attr->{PAGE_ROWS} ? $attr->{PAGE_ROWS} : 25; # Эта функция на основе пришедших параметров и паттерном формирует $WHERE clause. my $WHERE = $self->search_former($attr, [ [ 'ID', 'INT', 'pm.id', 1 ], [ 'NAME', 'STR', 'pm.name', 1 ], [ 'URL', 'STR', 'pm.url', 1 ], [ 'DATE', 'STR', 'DATE(pm.date) as date', 1 ], [ 'STATUS', 'INT', 'pm.status', 1 ], ], { WHERE => 1 }); $self->query( "SELECT $self->{SEARCH_FIELDS} pm.id FROM portal_menu pm $WHERE ORDER BY $SORT $DESC LIMIT $PG, $PAGE_ROWS;;", undef, $attr ); my $list = $self->{list} || []; # Берём общий count $self->query("SELECT COUNT(*) AS total FROM portal_menu pm $WHERE;", undef, { INFO => 1 } ); return $list || []; }
И вот как раз вы можете в $query_params при вызове делать условный /portal/menu/?url=call*&name=test, и оно соответственно создаст выражения для поиска.
Полностью автоматически!
А насчёт сортирования, есть специальные параметры:
SORT - параметр для сортировки
DESC - по возрастанию или убыванию
А пагинации и лимита:
PG - какая страница
PAGE_ROWS - лимит с таблицы
Это позволяет формировать единые экспрессии по всей системе, без ручного определения.