defmodule Tesla.Middleware.FormUrlencoded do
@moduledoc """
Send request body as `application/x-www-form-urlencoded`.
Performs encoding of `body` from a `Map` such as `%{"foo" => "bar"}` into
url encoded data.
Performs decoding of the response into a map when urlencoded and content-type
is `application/x-www-form-urlencoded`, so `"foo=bar"` becomes
`%{"foo" => "bar"}`.
## Examples
```
defmodule Myclient do
use Tesla
plug Tesla.Middleware.FormUrlencoded
end
Myclient.post("/url", %{key: :value})
```
## Options
- `:decode` - decoding function, defaults to `URI.decode_query/1`
- `:encode` - encoding function, defaults to `URI.encode_query/1`
## Nested Maps
Natively, nested maps are not supported in the body, so
`%{"foo" => %{"bar" => "baz"}}` won't be encoded and raise an error.
Support for this specific case is obtained by configuring the middleware to
encode (and decode) with `Plug.Conn.Query`
```
defmodule Myclient do
use Tesla
plug Tesla.Middleware.FormUrlencoded,
encode: &Plug.Conn.Query.encode/1,
decode: &Plug.Conn.Query.decode/1
end
Myclient.post("/url", %{key: %{nested: "value"}})
```
"""
@behaviour Tesla.Middleware
@content_type "application/x-www-form-urlencoded"
@impl Tesla.Middleware
def call(env, next, opts) do
env
|> encode(opts)
|> Tesla.run(next)
|> case do
{:ok, env} -> {:ok, decode(env, opts)}
error -> error
end
end
@doc """
Encode response body as querystring.
It is used by `Tesla.Middleware.EncodeFormUrlencoded`.
"""
def encode(env, opts) do
if encodable?(env) do
env
|> Map.update!(:body, &encode_body(&1, opts))
|> Tesla.put_headers([{"content-type", @content_type}])
else
env
end
end
defp encodable?(%{body: nil}), do: false
defp encodable?(%{body: %Tesla.Multipart{}}), do: false
defp encodable?(_), do: true
defp encode_body(body, _opts) when is_binary(body), do: body
defp encode_body(body, opts), do: do_encode(body, opts)
@doc """
Decode response body as querystring.
It is used by `Tesla.Middleware.DecodeFormUrlencoded`.
"""
def decode(env, opts) do
if decodable?(env) do
env
|> Map.update!(:body, &decode_body(&1, opts))
else
env
end
end
defp decodable?(env), do: decodable_body?(env) && decodable_content_type?(env)
defp decodable_body?(env) do
(is_binary(env.body) && env.body != "") || (is_list(env.body) && env.body != [])
end
defp decodable_content_type?(env) do
case Tesla.get_header(env, "content-type") do
nil -> false
content_type -> String.starts_with?(content_type, @content_type)
end
end
defp decode_body(body, opts), do: do_decode(body, opts)
defp do_encode(data, opts) do
encoder = Keyword.get(opts, :encode, &URI.encode_query/1)
encoder.(data)
end
defp do_decode(data, opts) do
decoder = Keyword.get(opts, :decode, &URI.decode_query/1)
decoder.(data)
end
end
defmodule Tesla.Middleware.DecodeFormUrlencoded do
@behaviour Tesla.Middleware
@impl true
def call(env, next, opts) do
opts = opts || []
with {:ok, env} <- Tesla.run(env, next) do
{:ok, Tesla.Middleware.FormUrlencoded.decode(env, opts)}
end
end
end
defmodule Tesla.Middleware.EncodeFormUrlencoded do
@behaviour Tesla.Middleware
@impl true
def call(env, next, opts) do
opts = opts || []
with env <- Tesla.Middleware.FormUrlencoded.encode(env, opts) do
Tesla.run(env, next)
end
end
end