Child pages
  • Программный интерфейс API
Skip to end of metadata
Go to start of metadata

С 1.22.00.

В данной статье описано программный интерфейс всего API, как и со стороны модуля так и ядра, так и флоу.


Принцип работы и флоу

C самого начала, api.cgi проверяет ли включена опция на сваггер и ли это был вызов на OpenAPI
Если да - клиент получает bundle_user.yaml или bundle_admin.yaml в зависимости от реквеста.

Загружается центральный английский словарь, если это не выставлено конфигурацией API_CONF_LANGUAGE

Создаётся главный объект Abills::Api::Handle, в который записывается вся необходимая информация для полной изоляции контекста.

Вызывается api_call, с которого мы получаем ответ, статус, и Content-Type (по умолчанию application/json; charset=utf-8, если это не определено иначе)

Внутри api_call, с получаемым контекстом производится проверка, ли API_ENABLE включена.
Если нет - клиент получает ответ, что API отключено.

Проверяется данные авторизации ADMIN API

Создаётся объект Abills::Api::Router, в который помещают весь полученный контекст.

При инициализации, проверяется ли HTTP метод соответствует полученому (GET, POST, PATCH, PUT, DELETE)
Если нет - клиент получает ответ с HTTP статусом 405.

Дальше разбивается url на несколько сегментов по /

Система запоминает первый сегмент
Запоминает второй сегмент, но если этот сегмент число - система запоминает третий сегмент.

Создаётся объект Abills::Api::Paths, который приобретает частичный контекст.

Система проверяет,
если HTTP метод это GET, DELETE - то создаётся и присваивается query_params.
если есть Content-Type: multipart/form-data, то создаётся и присваивается query_params, ведь body уже обработано
если есть буфер, то система попытается его декодировать с помощью JSON.
    если происходит ошибка декодирования - клиент получает ошибку декодирования и статус 400.
система сканирует query_params на предмет непозволительных символов, очищает буфер (для оптимизации памяти)

Дальше система проверяет первый сегмент URL - если это 'user', то попытается загрузить модуль или микромодуль как USER API на основе второго сегмента, и если загрузчик не определил модуль или микромодуль - система загружает User_core.pm
Если это не 'user' - загружает модуль или микромодуль как ADMIN API

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

Сам загрузчик получает с функции _extra_api_modules() названия микромодулей ядра, и смешивает с ними названия модулей.
Если префикса нет в этом списке - клиент получает 404.
На основе пришедших параметров (имя ресурса, 'user' или 'admin' api) попытается загрузить <module>::Api со всем заданным контекстом. Тоесть, первым приоритетом стоит название модуля.

Если загрузка успешная - создаёт объект и забирает список роутов.
Если нет - система попытается загрузить Abills::Api::Paths::<module>
   если загрузка успешная - создаёт объект и забирает список роутов
   если нет - клиент получает ошибку.
Список роутов определяется уже внутри объекта.
Изнутри объекта пробрасывается наружу словарь ошибок.

Если загружаемый модуль не оказался в списке модулей администратора - клиент получит 403.
Если загружаемого модуля не нашлось, система возвращает "остальные" системные пути на проверку.

После полной инициализации Abills::Api::Router система проверяет, ли есть за ним ошибка.
Если да - клиент получит ошибку от роутера, скорее всего 403 или 400.

В роутер иньектятся хуки на авторизацию (credentials).

Дальше вызывается функция handle, которая должна уже выполнить роут.
Но сначала внутри вызывается метод parse_request, который:

  1. Проверяет пришедший объект роута, если неправильный - переходит до следующего
  2. Парсит ключи внутри роута, подставляет их в path_params
  3. Проверяет опции на camelCase и придаёт им SCREAMING_UPPER_CASE
  4. Производит проверку формата
  5. Возвращает найденный путь, хэндлер (если он есть), path_params и query_params

Если роут не найден - клиент получает 404.
Если есть handler (старый метод) - запоминается.

На основе найденного пути производится вызов нужных хуков на авторизацию
Первый успешный хук позволяет системе пройти дальше.
Если хук на USER API - дополнительно в path_params присваивается uid.
Если нет успеха - клиент получает 401.

Если у роута есть схема валидации - система загрузит валидатор, и начнёт на основе данных производить проверку body.
Если проверка неуспешная - загружается словарь ошибок, процессится ошибка и возвращается клиенту, что схема не прошла по такой-то причине.

Система проверяет, если это ADMIN API, и в пути нужен uid, то система грузит модуль работы Users,
проверяет ли пользователь с таким uid существует.
Если нет - возвращается ошибка, что пользователя не существует.
Если есть - в path_params присваивается user_object.

Если есть у роута опция module (присуще для ядра), то система попытается его загрузить и присвоить как третий аргумент для хэндлера.
Если есть ошибка - клиент её получает.

Если у полученного роута есть опция controller и endpoint, то система попытается загрузить контроллер со всем наданным контекстом ПЛЮС встроенный словарь ошибок.

Загружаемый контроллер загружается с названием метода от опции endpoint
Если контроллера нет - загружается handler.

Хэндлер в пути запускается.

Если во время запуска случилась ошибка - клиент получает ошибку.
Если во время загрузки модуля ошибка - клиент получает ошибку.

Получаем конечный результат с хэндлера.
Присваиваем его роутеру.

Если результат пустой - присваивается пустой объект.
Система смотрит на полученный от роута content_type и присваивает его хэндлу.

Если авторизация успешная и хэндл успешный - система проверяет результат хэндлера, и старается удалить с него потенциально лишние данные.

Система на основе проверенных формирует JSON ответ.

Если включенно логирование - весь ответ и полученный реквест попадает в лог.

Клиент получает ответ сервера, статус и нужный content_type.

Объект пути

{
  # HTTP метод, GET, POST, PUT, PATCH, DELETE
  method      => 'POST',
  # Абсолютный путь, за которым можно будет достучаться, например billing.url/api.cgi/portal/articles
  # для USER API обязательно должно начинаться с /user/* 
  # В пути определяются основные параметры, по типу :id, :uid, которое отобразятся в пути
  path        => '/portal/articles/',
  # "Контроллер" для API /portal/articles/*
  controller  => 'Portal::Api::admin::Articles',
  # Ссылка на функцию-эндпойнт контроллера
  endpoint    => \&Portal::Api::admin::Articles::post_portal_articles,
  # Прямой хэндлер. Игнорируется, если присутствует controller и endpoint
  handler     => sub {
    # $path_params - параметры с путей
    #   если USER API и авторизация - обязательно присутствует uid
    #   если ADMIN API и uid - обязательно присутствует user_object
    # $query_params - параметры квери или боди
    #   если GET, DELETE - query параметры с URL
    #   иначе - body, form или JSON
    # $module_obj - если присутствует опция module, то это объект дополнительного загружаемого модуль.
    my ($path_params, $query_params, $module_obj) = @_
    # ...
  },
  # Дополнительный загружаемый модуль. Может сократить повторяющийся код.
  module => 'Fees',
  credentials => [
    # Параметры авторизации ADMIN API
    # ADMIN    - API_KEY
    # ADMINSID - admin_sid по cookie (в том числе для api_call)
    'ADMIN', 'ADMINSID'

    # Параметры авторизации USER API
    # USER    - через header, полученном с /user/login
    # USERSID - через cookie (в том числе для api_call)
    # USERBOT - через API Bots
    # PUBLIC  - без авторизации 

    # Тоесть, в данном случае путь может работать как и с авторизованными пользователями, так и нет.
    # Внутри хэндлера можно определять какой пользователь.
      ]
    },


Контроллер

# Пэкэдэ <Module>::Api::(admin | user)::<Controller>
package Portal::Api::admin::Articles;

=head1 NAME

  Portal articles manage

  Endpoints:
    /portal/articles/*

=cut

use strict;
use warnings FATAL => 'all';

# Словарь ошибок
use Control::Errors;

# Грузим нужные депенденси для этого контроллера
use Portal;

# Создаём объект словаря ошибок
my Control::Errors $Errors;

# Создаём объект(ы) депенденси
my Portal $Portal;

# Дополнительно: определяем permissions
my %permissions = ();

#**********************************************************
=head2 new($db, $admin, $conf)

=cut
#**********************************************************
sub new {
  my ($class, $db, $admin, $conf, $attr) = @_;

  my $self = {
    db    => $db,
    admin => $admin,
    conf  => $conf,
    attr  => $attr
  };

  %permissions = %{$attr->{permissions} || {}};

  bless($self, $class);

  # Определяем депенденси
  $Portal = Portal->new($db, $admin, $conf);

  # Забираем "предустановленный" словарь ошибок
  $Errors = $self->{attr}->{Errors};

  return $self;
}

# И дальше пишем пути #

1;


Определение пути

#**********************************************************
=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) = @_;
  # В $path_params находятся переменные которые определены путём
  # Например, /portal/articles/:id/ будет иметь параметр $path_params->{id}

  # в $query_params находятся параметры которые пришли с query
  # например: /portal/articles/?title=test
  # или c json/form body
  # например: POST /portal/articles { "title": "test" }

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

  # Стандартный результат при удалении, должен быть описан в Swagger 
  return $result;
}
  • No labels