- PVSM.RU - https://www.pvsm.ru -
Как реализовать на Эликсир JSON API endpoint без каких либо фреймворков?
От переводчика:
В статье приведён пример очень простого веб-приложения, которое можно рассматривать как Hello, World! в создании простейшего API на Эликсире.
Код примера незначительно изменён для того, чтобы соответствовать текущим версиям библиотек.
Полный код примера с изменениями можно увидеть на GitHub [1].

Многие разработчики приходят в Эликсир из мира Ruby. Это очень зрелая среда с точки зрения количества доступных библиотек и фреймворков. И такой зрелости мне иногда не хватает в Эликсире. Когда мне нужна сторонняя служба, результат поисков подходящей может быть следующим:

Вы, возможно, удивитесь, но Ruby не всегда на рельсах (Ruby on Rails, помните? — прим. переводчика). Связь с веб тоже не всегда обязана присутствовать. Хотя в данном конкретном случае давайте поговорим именно о вебе.
Когда дело доходит до реализации одной конечной точки RESTful (single RESTful endpoint), обычно есть множество вариантов:
Это примеры инструментов, которыми я лично пользовался. Мои коллеги — довольные пользователи Sinatra. Они успели пробовать и Hanami. Я могу выбрать любой устраивающий меня вариант даже в зависимости от моего текущего настроения.
Но когда я переключился на Эликсир оказалось, что выбор ограничен. Хотя существует несколько альтернативных “фреймворков” (названия которых по очевидным причинам я не буду здесь упоминать), использовать их почти невозможно!
Я провел весь день, разбираясь с каждой библиотекой, когда-либо упоминавшейся в Интернете. Действуя как Slack-бот, я пытался развернуть на Heroku простой сервер HTTP2, но к концу дня сдался. Буквально ни один из вариантов, что я нашел, не смог реализовать базовые требования.
Phoenix — мой самый любимый веб-фреймворк, просто иногда он избыточен. Не хотелось его использовать, подтягивая в проект весь фреймворк исключительно ради одной конечной точки; и неважно, что сделать это очень просто.
Не смог я воспользоваться и готовыми библиотеками, поскольку, как уже сказал, все найденные либо не подошли для моих нужд (требовалась базовая маршрутизация и поддержка JSON), либо не были достаточно удобны для легкого и быстрого развертывания на Heroku. "Сделаем шаг назад", — подумал я.

Но вообще-то и сам Phoenix построен на базе чего-то, не так ли?
Если необходимо создать на Ruby истинно минималистичный сервер, то можно просто воспользоваться rack — модульным интерфейсом для веб-серверов на Ruby.
К счастью, нечто подобное доступно и в Эликсире. В данном случае мы воспользуемся следующими элементами:
Я хочу реализовать компоненты вроде Endpoint (конечная точка), Router (маршрутизатор) и JSON Parser (обработчик JSON). Затем я хотел бы развернуть получившееся на Heroku и иметь возможность обрабатывать входящие запросы. Посмотрим, как этого можно достичь.
Убедитесь, что ваш проект на Эликсир содержит супервизор. Для этого проект нужно создать так:
mix new minimal_server --sup
Убедитесь, что mix.exs содержит:
def application do
[
extra_applications: [:logger],
mod: {MinimalServer.Application, []}
]
end
и создайте файл [11] lib/minimal_server/application.ex:
defmodule MinimalServer.Application do
use Application
def start(_type, _args),
do: Supervisor.start_link(children(), opts())
defp children do
[]
end
defp opts do
[
strategy: :one_for_one,
name: MinimalServer.Supervisor
]
end
end
В mix.exs необходимо указать следующие библиотеки:
defp deps do
[
{:poison, "~> 4.0"},
{:plug, "~> 1.7"},
{:cowboy, "~> 2.5"},
{:plug_cowboy, "~> 2.0"}
]
end
Затем скачайте и скомпилируйте зависимости:
mix do deps.get, deps.compile, compile
Теперь всё готово для создания точки входа на сервер. Давайте создадим файл lib/minimal_server/endpoint.ex со следующим содержимым:
defmodule MinimalServer.Endpoint do
use Plug.Router
plug(:match)
plug(Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Poison
)
plug(:dispatch)
match _ do
send_resp(conn, 404, "Requested page not found!")
end
end
Модуль Plug содержит Plug.Router [12] для перенаправления входящих запросов в зависимости от использованного пути и HTTP-метода. При получении запроса маршрутизатор вызовет модуль :match, представленный функцией match/2, отвечающей за поиск соответствующего маршрута, а затем перенаправит его в модуль :dispatch, который выполнит соответствующий код.
Поскольку мы хотим, чтобы наш API был JSON-совместимым, необходимо реализовать Plug.Parsers. Так как он обрабатывает запросы application/json с заданным :json_decoder, воспользуемся им для анализа тела запроса.
В результате мы создали временный маршрут "любой запрос", который соответствует всем запросам и отвечает кодом HTTP not found (404).
Реализация маршрутизатора будет последним шагом в создании нашего приложения. Это последний элемент всего конвейера, который мы создали: начиная с получения запроса от веб-браузера и заканчивая формированием ответа.
Маршрутизатор будет обрабатывать входящий запрос от клиента и отправлять назад какое-нибудь сообщение в нужном формате (добавьте приведённый код в файл lib/minimal_server/router.ex — прим. переводчика):
defmodule MinimalServer.Router do
use Plug.Router
plug(:match)
plug(:dispatch)
get "/" do
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Poison.encode!(message()))
end
defp message do
%{
response_type: "in_channel",
text: "Hello from BOT :)"
}
end
end
В приведённом выше модуле Router запрос будет обработан только если он отправлен методом GET и направлен по маршруту /. Модуль Router ответит с заголовком Content-Type, содержащим application/json и телом:
{
"response_type": "in_channel",
"text": "Hello from BOT :)"
}
Теперь настало время изменить модуль Endpoint для пересылки запросов маршрутизатору и доработать Application для запуска самого модуля Endpoint.
Первое можно сделать, добавив в MinimalServer.Endpoint [перед правилом match _ do ... end — прим. переводчика] строку
forward("/bot", to: MinimalServer.Router)
Это гарантирует, что все запросы к /bot будут направлены в модуль Router и обработаны им.
Второе можно реализовать, добавив в файл endpoint.ex функции child_spec/1 и start_link/1:
defmodule MinimalServer.Endpoint do
# ...
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]}
}
end
def start_link(_opts),
do: Plug.Cowboy.http(__MODULE__, [])
end
Теперь можно изменить application.ex, добавив MinimalServer.Endpoint в список, возвращаемый функцией children/0.
defmodule MinimalServer.Application do
# ...
defp children do
[
MinimalServer.Endpoint
]
end
end
Чтобы запустить сервер, достаточно выполнить:
mix run --no-halt
Наконец-то вы можете посетить адрес http://localhost:4000/bot [13] и увидеть наше сообщение :)

Чаще всего в локальной среде и для эксплуатации сервер настраивается по-разному. Поэтому нам нужно ввести отдельные настройки для каждого из этих режимов. Прежде всего изменим наш config.exs, добавив:
config :minimal_server, MinimalServer.Endpoint, port: 4000
В этом случае при запуске приложения в режиме test, prod и dev оно получит порт 4000, если эти настройки не изменить.
В этом месте автор оригинального текста забыл упомянуть, как доработать config.exs так, чтобы можно было использовать разные опции для разных режимов. Для этого необходимо в config/config.exs последней строкой добавить import_config "#{Mix.env()}.exs"; в результате получится что-то вроде:
use Mix.Config
config :minimal_server, MinimalServer.Endpoint, port: 4000
import_config "#{Mix.env()}.exs"
После этого в директории config создать файлы prod.exs, test.exs, dev.exs, поместив в каждый строку:
use Mix.Config
В продакшене мы обычно не хотим задавать номер порта жестко, а полагаемся на некоторую системную переменную окружающей среды, например:
config :minimal_server, MinimalServer.Endpoint,
port: "PORT" |> System.get_env() |> String.to_integer()
Добавьте текст выше в конец config/prod.exs — прим. переводчика
После этого локально использоваться будет фиксированное значение, а в рабочей эксплуатации — конфигурация из переменных среды.
Давайте внедрим эту схему в endpoint.ex, (заменив функцию start_link/1 — прим. переводчика):
defmodule MinimalServer.Endpoint do
# ...
require Logger
def start_link(_opts) do
with {:ok, [port: port] = config} <- Application.fetch_env(:minimal_server, __MODULE__) do
Logger.info("Starting server at http://localhost:#{port}/")
Plug.Adapters.Cowboy2.http(__MODULE__, [], config)
end
end
end
Heroku предлагает наипростейшее развертывание "в один клик" без какой-либо сложной настройки. Чтобы развернуть наш проект нужно подготовить пару простых файлов и создать удалённое приложение.

После установки Heroku CLI [14] можно создать новое приложение следующим образом:
$ heroku create minimal-server-habr
Creating ⬢ minimal-server-habr... done
https://minimal-server-habr.herokuapp.com/ | https://git.heroku.com/minimal-server-habr.git
Теперь добавьте к своему приложению набор для сборки Эликсира [15]:
heroku buildpacks:set
https://github.com/HashNuke/heroku-buildpack-elixir.git
На момент создания этого перевода текущими версиями Elixir и Erlang являются (плюс-минус):
erlang_version=21.1
elixir_version=1.8.1
Чтобы настроить сам набор для сборки добавьте строки выше в файл elixir_buildpack.config.
Последний шаг — создание Procfile, и, опять же, он очень прост:
web: mix run --no-halt
Примечание переводчика: чтобы избежать ошибки во время сборки на Heroku необходимо установить значение переменных окружения, которые используются в приложении:
$ heroku config:set PORT=4000
Setting PORT and restarting ⬢ minimal-server-habr... done, v5
PORT: 4000
Как только вы закоммитите новые файлы [с помощью git — прим. переводчика], можно выгрузить их на Heroku:
$ git push heroku master
Initializing repository, done.
updating 'refs/heads/master'
...
И это все! Приложение доступно по адресу https://minimal-server-habr.herokuapp.com [16].
К этому моменту вы уже поняли, как реализовать простейшее JSON RESTful API и HTTP-cервер на Эликсир без применения каких либо фреймворков, используя лишь 3 (4 — прим. переводчика) библиотеки.
Когда нужно обеспечить доступ к простым конечным точкам вам совершенно не нужно каждый раз использовать Phoenix, вне зависимости от того, насколько он клёвый, равно как и любой другой фреймворк.
Любопытно, почему отсутствуют надёжные, хорошо протестированные и поддерживаемые фреймворки где-то между plug + cowboy и Phoenix? Может быть, нет реальной необходимости реализовывать простые вещи? Может быть, каждая компания использует свою библиотеку? Или, возможно, все используют либо Phoenix, либо представленный подход?

Репозиторий [17], как всегда, доступен на моем GitHub.
Автор: heathen
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/api/312175
Ссылки в тексте:
[1] увидеть на GitHub: https://github.com/vheathen/elixir-http-json-api-mod
[2] моя собственная: https://github.com/KamilLelonek?utf8=%E2%9C%93&tab=repositories&q=&type=&language=elixir
[3] rack-app: https://github.com/rack-app/rack-app
[4] Hanami: http://hanamirb.org/
[5] Sinatra: http://www.sinatrarb.com/
[6] Grape: http://intridea.github.io/grape/
[7] Rails::API: https://github.com/rails-api/rails-api
[8] cowboy: https://github.com/ninenines/cowboy
[9] plug: https://github.com/elixir-plug/plug
[10] poison: https://github.com/devinus/poison
[11] файл: https://gist.github.com/KamilLelonek/c093f5e2776fbc97db2da2faae2c5d5c#file-application-ex
[12] Plug.Router: https://hexdocs.pm/plug/Plug.Router.html
[13] http://localhost:4000/bot: http://localhost:4000/bot
[14] установки Heroku CLI: https://devcenter.heroku.com/articles/heroku-cli#download-and-install
[15] набор для сборки Эликсира: https://github.com/HashNuke/heroku-buildpack-elixir
[16] https://minimal-server-habr.herokuapp.com: https://minimal-server-habr.herokuapp.com
[17] Репозиторий: https://github.com/KamilLelonek/elixir-http-json-api
[18] Источник: https://habr.com/ru/post/444554/?utm_campaign=444554
Нажмите здесь для печати.