Fork me on GitHub

Plug

Some contents of this translation may be outdated.
Several major changes were applied to the original lesson since the last update.

Se você estiver familiarizado com Ruby, você pode pensar sobre Plug como o Rack com uma pitada de Sinatra, ele fornece uma especificação para componentes de aplicação web e adaptadores para servidores web. Mesmo não fazendo parte do núcleo de Elixir, Plug é um projeto oficial de Elixir.

Sumário

Instalação

A instalação é uma brisa se você utilizar mix. Para instalar Plug nós precisamos fazer duas pequenas alterações no nosso mix.exs. A primeira coisa a fazer é adicionar tanto Plug quanto um servidor web para o nosso arquivo de dependências, vamos utilizar Cowboy:

defp deps do
  [{:cowboy, "~> 1.1.2"},
   {:plug, "~> 1.3.4"}]
end

A última coisa que nós precisamos fazer é adicionar tanto o nosso servidor web quanto o Plug na nossa aplicação OTP:

def application do
  [applications: [:cowboy, :logger, :plug]]
end

A especificação

A fim de começar a criar Plugs, nós precisamos saber e aderir a especificação Plug. Felizmente para nós, existem apenas duas funções necessárias: init/1 e call/2.

A função init/1 é usada para iniciar as opções do nosso Plug, passando como o segundo argumento para nossa função call/2. Além de nossas opções para inicialização da função call/2, a função recebe um %Plug.Conn como seu primeiro argumento, e é esperado que isto retorne uma conexão.

Aqui está um simples plug que retorna “Olá mundo!”:

defmodule HelloWorldPlug do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello World!")
  end
end

Criando um Plug

Para este exemplo, iremos criar um Plug para verificar se a requisição tem um conjunto de parâmetros necessários. Ao implementar a nossa validação em um Plug, podemos ter a certeza de que apenas os pedidos válidos serão gerenciados através de nosso aplicativo. Vamos esperar que o nosso Plug seja inicializado com duas opções: :paths e :fields. Estes irão representar os caminhos que aplicamos nossa lógica onde os campos são exigidos.

Note: Plugs são aplicadas a todas as requisições, e é por isso que nós iremos lidar com filtragem de solicitações e aplicar nossa lógica para apenas um subconjunto deles. Para ignorar um pedido simplesmente passamos a conexão através do mesmo.

Vamos começar analisando o Plug que acabamos de concluir, e em seguida, discutir como ele funciona, vamos criá-o em lib/plug/verify_request.ex:

defmodule Example.Plug.VerifyRequest do
  import Plug.Conn

  defmodule IncompleteRequestError do
    @moduledoc """
    Error raised when a required field is missing.
    """

    defexception message: "", plug_status: 400
  end

  def init(options), do: options

  def call(%Plug.Conn{request_path: path} = conn, opts) do
    if path in opts[:paths], do: verify_request!(conn.body_params, opts[:fields])
    conn
  end

  defp verify_request!(body_params, fields) do
    verified = body_params
               |> Map.keys
               |> contains_fields?(fields)
    unless verified, do: raise IncompleteRequestError
  end

  defp contains_fields?(keys, fields), do: Enum.all?(fields, &(&1 in keys))
end

A primeira coisa a ser notada é que definimos uma nova exceção IncompleteRequestError e que uma de suas opções é :plug_status. Quando disponível esta opção é usada pelo Plug para definir o código de status do HTTP no caso de uma exceção.

A segunda parte do nosso Plug é a função call/2, este é o lugar onde nós lidamos quando aplicar ou não nossa lógica de verificação. Somente quando o caminho do pedido está contido em nossa opção :paths iremos chamar verify_request/2.

A última parte do nosso Plug é a função privada verify_request!/2 no qual verifica quando os campos requeridos :fields estão todos presentes. No caso em que algum dos campos requeridos estiver em falta, nós acionamos IncompleteRequestError.

Usando Plug.Router

Agora que temos o nosso Plug VerifyRequest, podemos seguir para o nosso roteador. Como estamos prestes a ver, não precisamos de uma estrutura como Sinatra em Elixir, ganhamos isso de graça com Plug.

Para iniciar vamos criar um arquivo lib/plug/router.ex e copiar o seguinte código dentro deste:

defmodule Example.Plug.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/", do: send_resp(conn, 200, "Welcome")
  match _, do: send_resp(conn, 404, "Oops!")
end

Este é um Router mínimo, mas o código deve ser bastante auto-explicativo. Nós incluímos alguns macros através de use Plug.Router, e em seguida, configuramos dois Plugs nativos: :match e :dispatch. Existem duas rotas definidas, uma para manipulação retornos de GET para a raiz e a segunda para combinar todos os outros requests para que possamos retornar uma mensagem 404.

Vamos adicionar o nosso plug ao roteador:

defmodule Example.Plug.Router do
  use Plug.Router

  alias Example.Plug.VerifyRequest

  plug Plug.Parsers, parsers: [:urlencoded, :multipart]
  plug VerifyRequest, fields: ["content", "mimetype"],
                      paths:  ["/upload"]
  plug :match
  plug :dispatch

  get "/", do: send_resp(conn, 200, "Welcome")
  post "/upload", do: send_resp(conn, 201, "Uploaded")
  match _, do: send_resp(conn, 404, "Oops!")
end

É isso aí! Nós configuramos o nosso Plug para verificar se todas as requisições para /upload incluem tanto "content" quanto "mimetype", só então o código de rota irá ser executado.

Por agora nosso endpoint /upload não é muito útil, mas vimos como criar e integrar o nosso Plug.

Executando nosso Web App

Antes de podermos executar nossa aplicação nós precisamos instalar e configurar o nosso servidor web, neste caso Cowboy. Por agora vamos fazer as mudanças necessários no código para rodar tudo, e então vamos nos aprofundar em coisas específicas em lições futuras.

Vamos começar por atualizar parte de application do nosso mix.exs para especificar ao Elixir sobre a aplicação e variáveis de ambiente. Com essas alterações nosso código local deve parecer com isso:

def application do
  [applications: [:cowboy, :plug],
   mod: {Example, []},
   env: [cowboy_port: 8080]]
end

Em seguida, precisamos atualizar lib/example.ex para iniciar o supervisor para o Cowboy:

defmodule Example do
  use Application

  def start(_type, _args) do
    port = Application.get_env(:example, :cowboy_port, 8080)

    children = [
      Plug.Adapters.Cowboy.child_spec(:http, Example.Plug.Router, [], port: port)
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Agora para executar nossa aplicação, podemos usar:

$ mix run --no-halt

Testando Plugs

Testes em Plugs são bastante simples, graças ao Plug.Test, que inclui uma série de funções convenientes para fazer o teste ser algo fácil.

Veja se você consegue seguir o raciocínio do teste no router:

defmodule RouterTest do
  use ExUnit.Case
  use Plug.Test

  alias Example.Plug.Router

  @content "<html><body>Hi!</body></html>"
  @mimetype "text/html"

  @opts Router.init([])

  test "returns welcome" do
    conn = conn(:get, "/", "")
           |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 200
  end

  test "returns uploaded" do
    conn = conn(:post, "/upload", "content=#{@content}&mimetype=#{@mimetype}")
           |> put_req_header("content-type", "application/x-www-form-urlencoded")
           |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 201
  end

  test "returns 404" do
    conn = conn(:get, "/missing", "")
           |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 404
  end
end

Plugs disponíveis

Há um número de Plugs disponíveis e fáceis de utilizar, a lista completa pode ser encontrada na documentação do Plug neste link.


Compartilhe essa página