Jeżeli masz doświadczenie z Ruby to Plug może być czymś w rodzaju Racka z domieszką Sinatry. Definiuje on specyfikację dla aplikacji webowych oraz adapterów dla serwerów. Choć nie jest częścią biblioteki standardowej, to Plug jest oficjalnym projektem zespołu odpowiedzialnego za Elixira.
Spis treści
Instalacja
Instalacja z użyciem mix jest bardzo prosta. By zainstalować Plug musimy zmodyfikować plik mix.exs
. Pierwszą rzeczą do zrobienia jest dodanie Pluga oraz serwera web (wybraliśmy Cowboy) do pliku z zależnościami:
defp deps do
[{:cowboy, "~> 1.1.2"}, {:plug, "~> 1.3.4"}]
end
Następnie wystarczy tylko dodać serwer web oraz Plug do naszej aplikacji OTP:
def application do
[applications: [:cowboy, :logger, :plug]]
end
Specyfikacja
By tworzyć własne plugi musimy zapoznać się ze specyfikacją. Na całe szczęście istotne są tylko dwie funkcje: init/1
i call/2
.
Funkcja init/1
służy do inicjalizacji opcji pluga, które zostaną przekazane jako drugi argument funkcji call/2
. Dodatkowo w pierwszym argumencie funkcja call/2
otrzymuje %Plug.Conn
oraz musi zwrócić połączenie.
Oto prosty Plug zwracający “Hello World!”:
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
Tworzenie Pluga
W tym przykładzie stworzymy plug, który będzie sprawdzał czy żądanie zawiera wymagane parametry. Implementując własną walidację w plugu będziemy mieć pewność, że tylko poprawne żądania dotrą do aplikacji. Nasz plug będzie zainicjowany z dwiema opcjami: :paths
i :fields
. Będą one reprezentować ścieżkę żądania oraz pola wymagane dla tej ścieżki.
Uwaga: Plugi są wykonywane dla wszystkich żądań dlatego też będziemy musieli samodzielnie obsłużyć filtrowanie i wywoływać naszą logikę tylko w niektórych przypadkach. By zignorować żądanie wystarczy zwrócić połączenie.
Przyjrzyjmy się zatem naszemu gotowemu plugowi i zobaczmy jak on działa. Stworzyliśmy go w pliku 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
Na początku definiujemy nowy wyjątek IncompleteRequestError
który ma opcję :plug_status
. Opcja ta jest używana przez Plug by ustawić odpowiedni kod statusu w odpowiedzi HTTP gdy wystąpi wyjątek.
Drugim elementem naszego pluga jest metoda call/2
. To w niej decydujemy czy wykonana zostanie weryfikacja czy też pominiemy tę logikę. Tylko w przypadku gdy ścieżka żądania znajduje się w opcji :paths
wywołamy verify_request!/2
.
Ostatnim elementem jest prywatna funkcja verify_request!/2
, która sprawdza czy żądanie zawiera wszystkie pola wymienione w :fields
. Jeżeli jakieś pole nie istnieje wyrzuca wyjątek IncompleteRequestError
.
Użycie Plug.Router
Teraz gdy mamy nasz plug VerifyRequest
, możemy przejść do routera. Jak zaraz zobaczymy nie potrzebujemy dodatkowego narzędzia jak Sinatra, ponieważ w Elixirze mamy dostępny Plug.
Na początku stwórzmy plik lib/plug/router.ex
i skopiujmy do niego następujący kod:
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
Jest to minimalna konfiguracja lecz dzięki temu bardzo dobrze widać co się dzieje. Najpierw dołączyliśmy makro use Plug.Router
i następnie dwa wbudowane plugi: :match
i :dispatch
. Obsługujemy dwie ścieżki. Pierwsza to żądanie GET do strony głównej, a druga to wszystkie inne żądania, które zwrócą kod HTTP 404 z odpowiednią wiadomością
Dodajmy nasz plug do routera:
defmodule Example.Plug.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
I oto jest! Skonfigurowaliśmy nasz plug by obsługiwał żądania tak by dla ścieżki /upload
wymagane były pola "content"
i "mimetype"
. Tylko wtedy zostanie wykonany kod routera.
Na chwilę obecną adres /upload
nie jest specjalnie użyteczny, ale wiemy już jak stworzyć i włączyć nasz plug.
Uruchomienie aplikacji web
Zanim uruchomimy naszą aplikację web musimy skonfigurować serwer, w tym przypadku będzie to Cowboy. Będzie to konfiguracja minimum która pozwoli nam na uruchomienie aplikacji, szczegółami zajmiemy się w kolejnych lekcjach.
Rozpocznijmy od aktualizacji sekcji application
w pliku mix.exs
tak by wskazać Elixirowi naszą aplikację i ustawić jej zmienne środowiskowe. Po tych zmianach nasz plik powinien wyglądać mniej więcej tak:
def application do
[applications: [:cowboy, :plug], mod: {Example, []}, env: [cowboy_port: 8080]]
end
Następnie zaktualizujmy plik lib/example.ex
by uruchomić superwizora 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
I teraz by uruchomić naszą aplikację wystarczy wpisać:
$ mix run --no-halt
Testowanie Plugów
Testowanie plugów jest znacznie ułatwione dzięki modułowi Plug.Test
. Dostarcza on wiele przydatnych funkcji, które ułatwiają tworzenie testów.
Przyjrzyjmy się jak można przetestować nasz 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
Dostępne Plugi
Wiele plugów jest dostępnych od ręki w repozytorium. Kompletną listę można znaleźć w dokumentacji Pluga.
Caught a mistake or want to contribute to the lesson? Edit this page on GitHub!