Elixir:使用 Plug、Cowboy 和 Poison 构建小型 JSON 端点
很多时候,我只想在应用中添加一个简单的 JSON 端点,以便公开服务或处理 webhook 事件,而无需构建完整的框架。让我们看看使用Plug和Erlang 的Cowboy HTTP 服务器构建一个可用于生产的端点是多么简单。
插头是:
- Web 应用程序之间可组合模块的规范
- Erlang VM 中不同 Web 服务器的连接适配器
如果你来自Ruby/Rails,想想Rack,来自Node,想想Express等。当然,这些库的概念表面上相似,但它们本身都是独一无二的。
牛仔是:
适用于 Erlang/OTP 的小型、快速且现代的 HTTP 服务器
此外,它是一款支持 HTTP/2 的容错“现代 Web 服务器”,提供一套 WebSocket 处理程序和用于长连接接口。无需赘述,可以肯定地说,它是生产环境的可接受选择。更多信息,请参阅文档。
毒药是:
Elixir 的 JSON 库专注于极快的速度,同时不牺牲简单性、完整性或正确性。
换句话说,它是一个超快、可靠的 JSON 解析库。
构建端点
有了简短的定义,让我们构建一个端点来处理传入的 webhook 事件。现在,我们希望它能够“投入生产”,这对于我们的用例意味着什么?
- 容错:始终可用。永远不会宕机(至少不容易宕机 :)
- 易于配置:可以部署到任何环境
- 经过严格测试:让我们对所运送的货物充满信心
我们确实有一个非常简单的用例,在选择工具和投入时间寻找解决方案之前,了解您自己的需求是一个好主意。
1.创建一个新的、受监督的 Elixir 应用程序:
$ mix new webhook_processor --sup
$ cd webhook_processor
--sup
这将创建一个适合用作 OTP 应用程序的应用。OTP 和监管将满足我们上述的首要需求。我们的服务器将受到监管,并在崩溃时自动重启,而服务器可能会崩溃,而 Erlang VM 则不会(至少不容易崩溃 :)。
2. 添加 Plug、Cowboy 和 Poison 作为依赖项
# ./mix.exs
defmodule WebhookProcessor.MixProject do
use Mix.Project
def project do
[
app: :webhook_processor,
version: "0.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
# Add :plug_cowboy to extra_applications
extra_applications: [:logger, :plug_cowboy],
mod: {WebhookProcessor.Application, []}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:plug_cowboy, "~> 2.0"}, # This will pull in Plug AND Cowboy
{:poison, "~> 3.1"} # Latest version as of this writing
]
end
end
这里需要注意的是,我们在 中添加了plug_cowboy
(in deps
) 作为 Plug 和 Cowboy 的单一依赖项。我们也需要将其添加到(in ) 的列表:plug_cowboy
中。extra_applications
application
3. 混合 deps.get
$ mix deps.get
4. 实现 application.ex
# ./lib/webhook_processor/application.ex
defmodule WebhookProcessor.Application do
@moduledoc "OTP Application specification for WebhookProcessor"
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Use Plug.Cowboy.child_spec/3 to register our endpoint as a plug
Plug.Cowboy.child_spec(
scheme: :http,
plug: WebhookProcessor.Endpoint,
options: [port: 4001]
)
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: WebhookProcessor.Supervisor]
Supervisor.start_link(children, opts)
end
end
5. 实现 WebhookProcessor.Endpoint
# ./lib/webhook_processor/endpoint.ex
defmodule WebhookProcessor.Endpoint do
@moduledoc """
A Plug responsible for logging request info, parsing request body's as JSON,
matching routes, and dispatching responses.
"""
use Plug.Router
# This module is a Plug, that also implements it's own plug pipeline, below:
# Using Plug.Logger for logging request information
plug(Plug.Logger)
# responsible for matching routes
plug(:match)
# Using Poison for JSON decoding
# Note, order of plugs is important, by placing this _after_ the 'match' plug,
# we will only parse the request AFTER there is a route match.
plug(Plug.Parsers, parsers: [:json], json_decoder: Poison)
# responsible for dispatching responses
plug(:dispatch)
# A simple route to test that the server is up
# Note, all routes must return a connection as per the Plug spec.
get "/ping" do
send_resp(conn, 200, "pong!")
end
# Handle incoming events, if the payload is the right shape, process the
# events, otherwise return an error.
post "/events" do
{status, body} =
case conn.body_params do
%{"events" => events} -> {200, process_events(events)}
_ -> {422, missing_events()}
end
send_resp(conn, status, body)
end
defp process_events(events) when is_list(events) do
# Do some processing on a list of events
Poison.encode!(%{response: "Received Events!"})
end
defp process_events(_) do
# If we can't process anything, let them know :)
Poison.encode!(%{response: "Please Send Some Events!"})
end
defp missing_events do
Poison.encode!(%{error: "Expected Payload: { 'events': [...] }"})
end
# A catchall route, 'match' will match no matter the request method,
# so a response is always returned, even if there is no route to match.
match _ do
send_resp(conn, 404, "oops... Nothing here :(")
end
end
这看起来很多,但该文件的大部分内容只是一些有用的注释。要点是,我们使用宏get和post来Plug.Router
生成我们的路由。这个模块本身是一个插件,它定义了自己的插件管道。注意,match
和dispatch
是我们处理请求和分派响应所必需的。管道在这里是一个关键概念,因为插件的顺序决定了操作的顺序。请注意,匹配在我们定义解析器之前,这意味着除非有路由匹配,否则我们不会解析任何内容。如果顺序颠倒,无论路由是否匹配,我们都会尝试解析请求。有关更多信息,请参阅文档。Plug.Router
6.使端点可配置
# ./lib/webhook_processor/application.ex
defmodule WebhookProcessor.Application do
@moduledoc "OTP Application specification for WebhookProcessor"
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Use Plug.Cowboy.child_spec/3 to register our endpoint as a plug
Plug.Cowboy.child_spec(
scheme: :http,
plug: WebhookProcessor.Endpoint,
# Set the port per environment, see ./config/MIX_ENV.exs
options: [port: Application.get_env(:webhook_processor, :port)]
)
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: WebhookProcessor.Supervisor]
Supervisor.start_link(children, opts)
end
end
我们将 Cowboy 选项中硬编码的端口值替换成了环境变量,这样我们就可以在所需的任何环境中运行 webhook 处理器了。最后,为每个MIX_ENV
需要的配置创建一个配置文件:
#./config/config.exs
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
import_config "#{Mix.env()}.exs"
-------------------
# ./config/dev.exs
use Mix.Config
config :webhook_processor, port: 4001
-------------------
# ./config/test.exs
use Mix.Config
config :webhook_processor, port: 4002
-------------------
# ./config/prod.exs
use Mix.Config
config :webhook_processor, port: 80
7. 测试
# ./test/webhook_processor/endpoint_test.exs
defmodule WebhookProcessor.EndpointTest do
use ExUnit.Case, async: true
use Plug.Test
@opts WebhookProcessor.Endpoint.init([])
test "it returns pong" do
# Create a test connection
conn = conn(:get, "/ping")
# Invoke the plug
conn = WebhookProcessor.Endpoint.call(conn, @opts)
# Assert the response and status
assert conn.state == :sent
assert conn.status == 200
assert conn.resp_body == "pong!"
end
test "it returns 200 with a valid payload" do
# Create a test connection
conn = conn(:post, "/events", %{events: [%{}]})
# Invoke the plug
conn = WebhookProcessor.Endpoint.call(conn, @opts)
# Assert the response
assert conn.status == 200
end
test "it returns 422 with an invalid payload" do
# Create a test connection
conn = conn(:post, "/events", %{})
# Invoke the plug
conn = WebhookProcessor.Endpoint.call(conn, @opts)
# Assert the response
assert conn.status == 422
end
test "it returns 404 when no route matches" do
# Create a test connection
conn = conn(:get, "/fail")
# Invoke the plug
conn = WebhookProcessor.Endpoint.call(conn, @opts)
# Assert the response
assert conn.status == 404
end
end
这些测试非常简单,但它们可以确认我们的服务器是否按预期运行。有人可能会说,我们唯一应该关心的是这些测试的响应代码,而不是处理事件时产生的副作用。除非你编写的是集成式测试,否则测试应该始终在模块边界以内进行,切勿超出范围。
结论
我们几乎不费吹灰之力就构建了一个小巧但功能强大的端点。借助 Cowboy,您可以从一台服务器处理比实际所需更多的连接,因此,我们再将低成本添加到优势列表中。
那么部署呢?让我们来逐步了解如何构建发布版本并部署到 AWS:
与往常一样,代码可在 GitHub 上获取:https://github.com/jonlunsford/webhook_processor
文章来源:https://dev.to/jonlunsford/elixir-building-a-small-json-endpoint-with-plug-cowboy-and-poison-1826