DEV Community

Weerasak Chongnguluam
Weerasak Chongnguluam

Posted on

Elixir: จัดการ HTTP request ด้วย Plug

Plug เป็น Elixir library ที่ช่วยให้เราเขียนโค้ดจัดการ HTTP request แล้วส่ง response กลับไปให้ client ที่เรียกมาได้

สิ่งที่ชอบใน Plug คือการออกแบบที่แยกส่วนที่จัดการ HTTP connection ออกไปโดยกำหนด spec Adapter behavior ขึ้นมาให้มี function callback ตามที่ต้องการ แล้วค่อยสร้าง Adapter module มา implements callback ให้ต่อ HTTP connection จริงๆได้ ซึ่ง Adapter ที่ใช้กันส่วนใหญ่ก็คือ Plug.Cowboy ที่ด้านล่างใช้ cowboy library นั่นเอง

ส่วนที่สำคัญและทำให้การจัดการ request/response ง่ายมากเวลาใช้ Plug คือ ส่วนจัดการ request/response module ที่มี spec แบบตามนี้ ตัวอย่างเช่น

defmodule MyPlug do
  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    conn
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, "Hello")
  end
end
Enter fullscreen mode Exit fullscreen mode

คือเป็น module ที่ต้องมี function init/1 ที่รับค่า option แล้วสามารถ process options แล้ว return options ที่ process แล้วออกไป

และต้องมี function call/2 ที่ค่าแรกจะรับ conn ซึ่งเป็นค่า Plug.Conn กับ options ที่ได้จาก init/1

Plug.Conn

Plug.Conn เป็น struct ที่สำคัญในกลไกการทำงานของ Plug เพราะเป็น struct ที่รวบรวม fields ต่างๆสำหรับเก็บข้อมูล request/response เอาไว้ และ Plug.Conn ก็ยังเป็น module ที่รวบรวม function ที่เอาไว้จัดการ fields ต่างๆของ Plug.Conn เอาไว้ หลักๆแล้วเราจัดการกับ HTTP ผ่าน function ของ Plug.Conn นั่นเอง เช่นตามตัวอย่างเราสามารถกำหนด response content type header ผ่านทาง Plug.Conn.put_resp_content_type/2 และส่ง response body ผ่านทาง Plug.Conn.send_resp(200, "Hello") นั่นเอง

Plug pipeline

อีก concept คือ Plug pipeline คือการที่เราสร้าง Plug ย่อยๆแล้วเอามาร้อยเรียงกัน ภาษาหรือ library อื่นอาจจะเรียกแบบนี้ว่า middleware

ตัว plug pipeline เองมันก็คือ Plug module เหมือนกันนี่แหละแต่เอา Plug ที่มีอยู่แล้วมาประกอบกัน สำหรับ library Plug มี module Plug.Builder ช่วยให้เราประกอบ pipeline ได้ง่ายๆด้วย macro plug/2 เช่น

defmodule MyHelloPlug do
  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    conn
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, "Hello")
  end
end

defmodule LogPlug do
  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    Time.utc_now()
    |> IO.puts()

    conn
  end
end

defmodule MyPlug do
  use Plug.Builder

  plug(LogPlug)
  plug(MyHelloPlug)
end

Enter fullscreen mode Exit fullscreen mode

เวลาทำงานก็เรียก แต่ละ Plug ตามลำดับที่เราเรียก plug/2 นั่นเอง

นอกจากนั้นถ้าสร้าง Plug ผ่าน Plug.Builder เรายัง implements plug ผ่าน function ได้เลยไม่ต้องใช้ Module คือขอให้เป็น function ที่รับค่า conn กับ options แบบเดียวกับ call function นั่นเอง ดังนั้นโค้ดเมื่อกี้เขียนใหม่ได้เป็น

defmodule MyPlug do
  use Plug.Builder

  plug(:hello_plug)
  plug(:log_plug)

  def hello_plug(conn, _opts) do
    conn
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, "Hello")
  end

  def log_plug(conn, _opts) do
    Time.utc_now()
    |> IO.puts()

    conn
  end
end
Enter fullscreen mode Exit fullscreen mode

สิ่งที่ได้เรียนรู้ก็คือเราจะเห็นการออกแบบ แบบนี้ของ Elixir อีกครั้งใน library Plug คือการพยายามผลักกลไก low level ออกไปเป็น module ใหม่แล้วทำให้ส่วนหลักที่เป็น logic ที่เราต้อง implements นั้นใช้งานง่ายๆ การจัดการ HTTP ก็แค่การพยายาม transforms ข้อมูลใน Plug.Conn ต่อกันไปเรื่อยๆใน Plug pipeline

การทดสอบก็จะง่ายเพราะก็เป็นการเช็คของที่อยู่ใน Plug.Conn นั่นเอง หรือแม้แต่การสร้าง Test Adapter เอาไว้ทดสอบก็ยังได้

Top comments (0)