Fork me on GitHub

Plug

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.0.0"},
   {:plug, "~> 1.0"}]
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/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

  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.


Podziel się