In the world of Elixir
, Plug
is a specification that allows different frameworks to communicate with different web servers running in Erlang VM
.
If you are familiar with Ruby
, you can draw an analogy with Rack
: Plug
tries to solve the same problems, but in a different way. Understanding the basics of Plug
will help you better understand both Phoenix
and other web frameworks created in the Elixir
language.
You can think of Plug
as a piece of code that takes a data structure, performs some transformations with it, and returns the same data structure, but already partially modified. The data structure that the Plug
works with is usually called a connection . This structure stores everything you need to know about the request ( lane : and the answer, too).
Since any Plug
accepts and returns a , it is possible to build a chain of several such objects that will sequentially handle the same
. This composition is called
Plug pipeline
The data structure itself, representing the is a regular
Elixir
structure called %Plug.Conn{}
(documentation on it can be found here ).
There are two different types of Plug
: Plug
function and Plug
module.
Plug
function is any function that accepts a as an argument (this is the same
%Plug.Conn{}
), and a set of options, and returns the .
def my_plug(conn, opts) do conn end
Plug
module is in turn any module that has the following interface: init/1
and call/2
, implemented as follows:
module MyPlug do def init(opts) do opts end def call(conn, opts) do conn end end
Of interest is the fact that the init/1
function is called at compile time, and the call/2
function is call/2
while the program is running.
Let's move from theory to practice and create a simple application that uses Plug
to process an http
request.
First, create a new project using mix
:
$ mix new learning_plug $ cd learning_plug
Edit the mix.exs
file, adding Plug
and Cowboy
as dependencies (this is a web server):
# ./mix.exs defp deps do [{:plug, "~> 1.0"}, {:cowboy, "~> 1.0"}] end
We will tighten dependencies:
$ mix deps.get
and we are ready to start work!
Our first Plug
will simply return "Hello, World!" :
defmodule LearningPlug do # The Plug.Conn module gives us the main functions # we will use to work with our connection, which is # a %Plug.Conn{} struct, also defined in this module. import Plug.Conn def init(opts) do # Here we just add a new entry in the opts map, that we can use # in the call/2 function Map.put(opts, :my_option, "Hello") end def call(conn, opts) do # And we send a response back, with a status code and a body send_resp(conn, 200, "#{opts[:my_option]}, World!") end end
To use this module, run iex
with the project environment:
$ iex -S mix
and execute the following commands:
iex(1)> Plug.Adapters.Cowboy.http(LearningPlug, %{}) {:ok, #PID<0.150.0>}
We use Cowboy
as a web server, telling it to use our Plug. The second argument of the http/2
function (in this case, an empty Map
%{}
) is the same set of options that will be passed as an argument to the init/1
function in our Plug
.
The web server was supposed to start on port 4000, so if you open http://localhost:4000
in a browser, you will see "Hello, World!" . Very simple!
Let's try to make our Plug
little smarter. Let him analyze the URL to which we make a request to the server, and if for example we are trying to access http://localhost:4000/Name
we should see “Hello, Name” .
Since the is figuratively everything you need to know about the request, it also stores its URL. We can simply match the pattern of this URL to create the response we want. Let's slightly alter the
call/2
function as follows:
def call(%Plug.Conn{request_path: "/" <> name} = conn, opts) do send_resp(conn, 200, "Hello, #{name}") end
Here it is, the power of functional programming! We only match the information we need (name), and then use it to generate a response.
Plug
in itself is of no particular interest. The beauty of this architecture is revealed when trying to compose multiple Plug
modules together. Each of them does its little part of the work, and passes the on.
Phoenix
framework uses the pipeline
everywhere, and does it very cleverly. By default, to process a regular browser request, the pipeline
looks like this:
pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end
If, for example, we need to process a request to the API , we do not need most of these functions. Then the pipeline
greatly simplified:
pipeline :api do plug :accepts, ["json"] end
Of course, the pipeline
macro from the previous example is built into Phoenix
. However, the Plug
itself provides the ability to build such a pipeline
: Plug.Builder
.
Here is an example of his work:
defmodule MyPipeline do # We use Plug.Builder to have access to the plug/2 macro. # This macro can receive a function or a module plug and an # optional parameter that will be passed unchanged to the # given plug. use Plug.Builder plug Plug.Logger plug :extract_name plug :greet, %{my_option: "Hello"} def extract_name(%Plug.Conn{request_path: "/" <> name} = conn, opts) do assign(conn, :name, name) end def greet(conn, opts) do conn |> send_resp(200, "#{opts[:my_option]}, #{conn.assigns.name}") end end
Here we made the composition of three Plug
- Plug.Logger
, extract_name
and greet
modules.extract_name
uses assign/3
to put a value with a specific key into the .
assign/3
returns a modified copy of the , which is then processed by
greet_plug
, which reads the value backwards, to then generate the response we need.
Plug.Logger
comes with a Plug
and, you guessed it, used to log http
requests. Directly out of the box is available a certain set of "batteries", the list can be found here.
Using such a pipeline
is as easy as Plug
:
Plug.Adapters.Cowboy.http(MyPipeline, %{})
It should be remembered that the modules are used in the same sequence in which they are defined in the pipeline
Another feature: those compositions created with Plug.Builder
also implement the Plug
interface. Therefore, for example, you can make a composition of the pipeline
and Plug
, and continue indefinitely!
The basic idea is that both the request and the response are presented in one %Plug.Conn{}
common structure, and this structure is passed "along the chain" from function to function, partially changing at each step ( trans : figuratively changing - data is immuniable, therefore, a modified copy of the structure is transmitted further, until a response is received that will be sent back. Plug
is a specification that defines how it should work and creates abstractions so that different frameworks can communicate with different web servers as long as they fulfill this specification.
The "batteries" for the Plug
include various modules that facilitate many different common tasks: the creation of a pipeline
, simple routing, cookies, headers, and so on.
And in the end I would like to note that the basis of the Plug
is the very idea of ​​functional programming - the transfer of data along a chain of functions that transform this data until the desired result is obtained. Just in this case, the data is an http
request.
Update: in fact, this is a translation, for some reason it was created as a publication. Correct, apologize. Additionally - minor spelling corrections.
Source: https://habr.com/ru/post/306334/
All Articles