Plug
Si estás familiarizado con Ruby puedes imaginar que Plug es como Rack con un poquito de Sinatra. Este proporciona una especificación para componentes de aplicaciones web y adaptadores para servidores web. Si bien no forma parte del núcleo de Elixir, Plug es un proyecto oficial de Elixir.
Empezaremos creando una aplicación web mínima basada en Plug Despues de eso, aprenderemos acerca del enrutador de Plug y como agregar Plug a una aplicación web existente.
Prerrequisitos
Este tutorial asume que ya tienes Elixir y mix
instalado.
Si no has iniciado un proyecto, crea uno de la siguiente manera:
mix new example
cd example
Instalación
La instalación es cosa fácil con mix.
Para instalar Plug tenemos que hacer dos pequeños cambios en nuestro mix.exs
.
Lo primero que se debe hacer es añadir Plug y un servidor web (usaremos Cowboy) en nuestro archivo como dependencias:
defp deps do
[{:cowboy, "~> 1.1.2"}, {:plug, "~> 1.3.4"}]
end
En la línea de comando, ejecuta la siguiente tarea de mix para actualizar estas nuevas dependencias:
mix deps.get
La especificación
Para comenzar a crear Plugs, necesitamos conocer, y adherirse a la especificación Plug.
Afortunadamente para nosotros, sólo hay dos funciones necesarias: init/1
y call/2
.
Aquí hay un Plug simple que devuelve “Hello World!”:
defmodule Example.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
Guarda el archivo en lib/example/hello_world_plug.ex
.
La función init/1
se utiliza para inicializar las opciones de nuestros Plugs. Esta es llamada por el árbol de supervisión, el cual se explica en la siguiente sección. De momento, está será una lista vacía que es ignorada.
El valor retornado por la función init/1
eventualmente será pasado a call/2
como su segundo argumento.
La función call/2
es ejecutada por cada petición que viene desde el servidor web, Cowboy. Esta recibe una estructura de conexión %Plug.Conn{}
como su primer argumento y se espera que retorne una estructura de conexión %Plug.Conn{}
.
Configurando el Módulo de Aplicación del proyecto
Debido a que estamos iniciando nuestra aplicación plug desde cero, necesitamos definir el módulo de la aplicación.
Actualiza lib/example.ex
para iniciar y supervisar Cowboy:
defmodule Example do
use Application
require Logger
def start(_type, _args) do
children = [
Plug.Adapters.Cowboy.child_spec(:http, Example.HelloWorldPlug, [], port: 8080)
]
Logger.info("Started application")
Supervisor.start_link(children, strategy: :one_for_one)
end
end
Esto supervisa Cowboy, y a su vez, supervisa nuestro HelloWorldPlug
.
En la petición a Plug.Adapters.Cowboy.child_spec/4
, el tercer argumento será pasado a Example.HelloWorldPlug.init/1
.
Aún no hemos terminado. Abre mix.exs
de nuevo, y busca la función applications
.
De momento la parte de aplication
en mix.exs
necesita dos cosas:
-
Una lista de aplicaciones de dependencia (
cowboy
,logger
, andplug
) que necesintan iniciar, y - Configuración para nuestra aplicación, la cual también deberá iniciar automáticamente. Vamos a actualizarla para hacerlo:
def application do
[
extra_applications: [:cowboy, :logger, :plug],
mod: {Example, []}
]
end
Estamos listos para probar este servidor web, minimalístico basado en Plug. En la línea de comando ejecuta:
mix run --no-halt
Cuando todo termine de compilar, y el mensaje [info] Started app
aparece, abre el explorador web en 127.0.0.1:8080
. Este debera de desplegar:
Hello World!
Plug.Router
Para la mayorìa de aplicaciones, como un sitio web o un API REST, necesitaras un enrutador que enrute las solicitudes para las distintas rutas y verbos HTTP hacia los distintos manejadores.
Plug
provee un enrutador para hacer esto. Como estamos a punto de ver, no necesitamos un framework como Sinatra en Elix ya que lo conseguimos fácilmente con Plug.
Para empezar vamos a crear un archivo en lib/example/router.ex
y copia lo siguiente en el mismo:
defmodule Example.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 es un router simple y básico pero el código debería explicarse por sí mismo.
Hemos incluido algunas macros a través de use Plug.Router
y luego configurado dos de los Plugs incorporados::match
y :dispatch
.
Hay dos rutas definidas, una para el manejo de GET que retorna a la raíz y la segunda para hacer coincidir todas las demás solicitudes para que podamos devolver un mensaje 404.
De vuelta en lib/example.ex
, necesitamos agregar Example.Router
en el árbol de supervisión del servidor web.
Cambia el plug Example.HelloWorldPlug
al nuevo enrutador:
def start(_type, _args) do
children = [
Plug.Adapters.Cowboy.child_spec(:http, Example.Router, [], port: 8080)
]
Logger.info("Started application")
Supervisor.start_link(children, strategy: :one_for_one)
end
Inicia el servidor de nuevo, detén el anterior si aún sigue corriendo.(Presiona Ctrl+C
dos veces).
Ahora en un navegador web ve a 127.0.0.1:8080
.
Deberá de mostrar Welcome
.
Ahora, ve a 127.0.0.1:8080/waldo
, o cualquier otra ruta.
Esta deberá de mostrar Oops!
con una repuesta 404.
Agregando otro Plug
Es muy común crear Plugs que intercepten todas las peticiones o un conjunto de estas, para controlar la lógica de manejo de peticiones comunes.
Para este ejemplo vamos a crear un plug que verifica si la solicitud tiene algún conjunto de parámetros requeridos.
Mediante la implementación de nuestra validación en un Plug podemos estar seguros de que sólo las solicitudes válidas se hacen a través de nuestra aplicación.
Esperamos que nuestro Plug sea inicializado con dos opciones: :paths
y :fields
. Estas representarán las rutas que aplicamos a nuestra lógica y los campos que se requieren.
Nota: Los Plugs son aplicados a todas las peticiones y por eso vamos a manejar solicitudes filtradas y aplicaremos nuestra lógica a sólo un subconjunto de ellas. Para ignorar una petición, simplemente la pasamos a través de la conexión.
Vamos a empezar por mirar nuestro Plug terminado y entonces discutiremos cómo funciona, lo crearemos en lib/example/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
La primera cosa a destacar es que hemos definido una nueva excepción IncompleteRequestError
y una de sus opciones es :plug_status
.
Cuando esté disponible esta opción será utilizada por Plug para establecer el código de estado HTTP en el caso de una excepción.
La segunda parte de nuestro Plug es la función call/2
.
Aquí es donde nos encargamos de decidir si aplicaremos nuestra lógica de verificación.
Sólo cuando la ruta de la solicitud figure en nuestro :paths
vamos a llamar verify_request!/2
.
La última parte de nuestro plug es la función privada verify_request!/2
que verifica si los :fields
requeridos están todos presentes.
En el caso de que falte alguno, levantamos la excepción IncompleteRequestError
.
Hemos configurado nuestro Plug para verficar que todas las peticiones a /upload
incluyan tanto "content"
como "mimetype"
.
Solo en estos casos el código del router será ejecutado.
Ahora, le indicamos al router del nuevo Plug.
Edita lib/example/router.ex
y realiza los siguientes cambios:
defmodule Example.Router do
use Plug.Router
use Plug.ErrorHandler
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
Haciendo que el Puerto HTTP sea Configurable
De vuelta cuando definimos el modulo y la aplicación Example
, el puerto estaba quemado en el módulo.
Se considera una buena práctica hacer que el puerto sea configurado incluyéndolo en el archivo de configuración.
Empecemos actualizando la porción de application
en mix.exs
para indicar a Elixir acerca de nuestra aplicación y especificar una variable de entorno de aplicación.
Con esos cambios listos nuestro código debe de verse similar a este:
def application do
[applications: [:cowboy, :logger, :plug], mod: {Example, []}, env: [cowboy_port: 8080]]
end
Nuestra aplicación es configurada con la línea mod: {Example, []}
.
Debes de notar que tambien estamos inicializando las aplicaciones cowboy
, logger
y plug
.
Ahora necesitamos actualizar lib/example.ex
para la lectura del valor de configuración del puerto, y pasarlo a 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
El tercer argumento the Application.get_env
es el valor predeterminado, para cuando la directiva de configuración no esté definida.
(Opcional) agregar
:cowboy_port
enconfig/config.exs
use Mix.Config
config :example, cowboy_port: 8080
Ahora para correr nuestra aplicación podemos utilizar:
mix run --no-halt
Probando un Plug
Probar un plug es muy sencillo gracias a Plug.test
.
Este incluye un número de funciones convenientes que facilitan las pruebas.
Comprueba si puedes darle seguimiento a la pueba del router:
defmodule Example.RouterTest do
use ExUnit.Case
use Plug.Test
alias Example.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 disponibles
Hay una serie Plugs disponibles por defecto. La lista completa se puede encontrar en la documentación de Plug aquí.
¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!