Plug
Kalau anda familiar dengan Ruby anda bisa menganggap Plug seperti Rack dengan sedikit Sinatra. Plug memberi spesifikasi untuk komponen aplikasi web dan adapter untuk web server. Walau bukan bagian Elixir core, Plug adalah sebuah project resmi Elixir.
Instalasi
Instalasi menggunakan Mix sangat mudah. Untuk menginstal Plug kita perlu membuat dua perubahan kecil pada mix.exs
kita. Yang pertama perlu dilakukan adalah menambahkan Plug dan sebuah web server (kita akan pakai Cowboy) ke file kita sebagai dependensi:
defp deps do
[{:cowboy, "~> 1.1.2"}, {:plug, "~> 1.3.4"}]
end
Yang terakhir kita perlu lakukan adalah menambahkan web server kita dan Plug ke aplikasi OTP kita:
def application do
[applications: [:cowboy, :logger, :plug]]
end
Spesifikasi
Untuk memulai membuat Plug, kita perlu tahu, dan menuruti, spesifikasi Plug. Untungnya, hanya ada dua fungsi yang diperlukan: init/1
dan call/2
.
Fungsi init/1
digunakan untuk menginisialisasi opsi-opsi Plug kita, yang dimasukan sebagai argumen kedua untuk fungsi call/2
kita. Di samping opsi inisialisasi itu fungsi call/2
menerima sebuah %Plug.Conn
sebagai argumen pertamanya dan mengembalikan sebuah connection.
Ini adalah sebuah Plug sederhana yang mengembalikan “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
Membuat sebuah Plug
Untuk contoh ini kita akan membuat sebuah Plug untuk memverifikasi apakan request memiliki parameter yang dibutuhkan. Dengan mengimplementasikan validasi kita dalam sebuah Plug kita bisa yakin bahwa request yang valid yang mencapai aplikasi kita. Kita akan mempunyai ekspektasi bahwa Plug kita diinisialisasi dengan dua opsi: :paths
dan :fields
. Kedua opsi ini akan merepresentasikan path yang akan kita terapkan logika kita atasnya dan field apa saja yang dibutuhkan.
Catatan: Plug diterapkan pada semua request sehingga kita akan menerapkan logika kita hanya pada sebagian dari request tersebut. Untuk mengabaikan sebuah request kita hanya akan meneruskannya.
Kita akan mulai dengan melihat pada Plug kita yang sudah jadi dan kemudian mendiskusikan bagaimana ia bekerja. Kita akan membuatnya di 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
Hal pertama yang perlu dicatat adalah bahwa kita telah mendefinisikan sebuah exception baru IncompleteRequestError
dan bahwa salah satu opsinya adalah :plug_status
. Jika tersedia opsi ini digunakan oleh Plug untuk menset kode status HTTP jika terjadi exception.
Bagian kedua dari Plug kita adalah fungsi call/2
. Di sinilah kita memutuskan apakah akan menerapkan logika verifikasi kita atau tidak. Hanya jika path dari request tersebut ada dalam opsi :paths
kita sajalah kita akan memanggil verify_request!/2
.
Bagian terakhir dari plug kita adalah fungsi privat verify_request!/2
yang memverifikasi apakah :fields
yang dibutuhkan semuanya ada. Jika ada yang tidak ada, kita memunculkan exception IncompleteRequestError
.
Menggunakan Plug.Router
Sekarang setelah kita memiliki plug VerifyRequest
kita, kita bisa lanjutkan ke router kita. Sebagaimana akan kita lihat, kita tidak butuh sebuah framework seperti Sinatra dalam Elixir karena kita sudah dapatkan gratis dari Plug.
Untuk memulai mari buat sebuah file di lib/plug/router.ex
dan salin code berikut ini:
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
Ini adalah sebuah Router yang minimum tapi code nya mestinya sudah jelas. Kita meng-include beberapa macro melalui use Plug.Router
dan kemudian menset dua Plug yang built-in: :match
dan :dispatch
. Ada dua route yang didefinisikan, satu untuk menangani request GET ke root dan yang kedua untuk menangani semua request lain sehingga kita bisa mengembalikan sebuah pesan 404.
Mari tambahkan Plug kita ke router tersebut:
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
Beres! Kita sudah mensetup Plug kita untuk memverifikasi bahwa semua request ke /upload
menyertakan "content"
dan "mimetype"
. Hanya jika itu terpenuhi route code nya dijalankan.
Sementara ini endpoint /upload
kita tidak begitu berguna tetapi kita sudah melihat bagaimana membuat dan mengintegrasikan Plug kita.
Menjalankan Web App Kita
Sebelum kita bisa menjalankan aplikasi kita kita perlu mensetup dan mengkonfigurasi web server kita, yang dalam hal ini adalah Cowboy. Untuk saat ini kita akan hanya membuat perubahan code yang perlu untuk menjalankan semuanya, dan kita akan perdalam di pelajaran-pelajaran berikutnya.
Mari mulai dengan mengubah bagian application
dari mix.exs
kita untuk memberitahu Elixir tentang apliksi kita dan menset sebuah environment variable. Dengan perubahan itu code kita seharusnya tampak seperti berikut:
def application do
[applications: [:cowboy, :plug], mod: {Example, []}, env: [cowboy_port: 8080]]
end
Kemudian kita perlu mengubah lib/example.ex
untuk menjalankan dan mensupervisi 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
Lalu untuk menjalankan aplikasi kita kita bisa gunakan:
mix run --no-halt
Menguji Plug
Menguji Plug adalah mudah berkat Plug.Test
. Plug.Test
menyertakan sejumlah fungsi yang mempermudah pengujian.
Coba kita lihat apakah anda bisa pahami test router ini:
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
Plug yang Tersedia
Ada sejumlah Plug yang sudah disertakan. Daftar lengkapnya dapat dilihat di dokumentasi Plug di sini.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!