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.

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 lesson on GitHub!