Packages

Tesla middleware for encoding requests and decoding responses as XML.

Current section

Files

Jump to
tesla_middleware_xml lib tesla_middleware_xml.ex
Raw

lib/tesla_middleware_xml.ex

# https://github.com/elixir-tesla/tesla/blob/master/lib/tesla/middleware/json.ex
defmodule Tesla.Middleware.XML do
@moduledoc """
Encode requests and decode responses as XML.
This middleware can be configured to use different engines for the encoding
and decoding of XML requests and responses.
Remember to add `{:saxy, "~> 1.5"}` (or another supported engine) to the
dependencies of your project.
If you only need to encode the request body or decode the response body,
you can use `Tesla.Middleware.EncodeXml` or `Tesla.Middleware.DecodeXml`
directly instead.
### Examples
```elixir
defmodule MyClient do
use Tesla
plug Tesla.Middleware.XML # use default settings
# or
plug Tesla.Middleware.XML, engine: XmlJson, engine_opts: [convention: AwsApi]
end
```
### Options
- `:decode` - decoding function
- `:decode_content_types` - list of additional decodable content-types
- `:encode` - encoding function
- `:encode_content_type` - content-type to be used in request header
- `:engine` - encode/decode engine, eg. `Saxy`, `XmlJson` (defaults to `Parker`)
- `:engine_opts` - optional engine opts
"""
@behaviour Tesla.Middleware
@default_content_types ["application/xml"]
@default_encode_content_type "application/xml"
@default_engine XmlJson
@impl Tesla.Middleware
def call(env, next, opts) do
opts = opts || []
with {:ok, env} <- encode(env, opts),
{:ok, env} <- Tesla.run(env, next) do
decode(env, opts)
end
end
@doc """
Encode request body as XML.
It is also used by `Tesla.Middleware.EncodeXML`.
"""
@spec encode(Tesla.Env.t(), keyword()) :: Tesla.Env.result()
def encode(env, opts) do
with true <- encodable?(env),
{:ok, body} <- encode_body(env.body, opts) do
{:ok,
env
|> Tesla.put_body(body)
|> Tesla.put_headers([{"content-type", encode_content_type(opts)}])}
else
false -> {:ok, env}
error -> error
end
end
defp encode_body(%Stream{} = body, opts), do: {:ok, encode_stream(body, opts)}
defp encode_body(body, opts) when is_function(body), do: {:ok, encode_stream(body, opts)}
defp encode_body(body, opts), do: process(body, :encode, opts)
defp encode_content_type(opts),
do: Keyword.get(opts, :encode_content_type, @default_encode_content_type)
defp encode_stream(body, opts) do
Stream.map(body, fn item ->
{:ok, body} = encode_body(item, opts)
body <> "\n"
end)
end
defp encodable?(%{body: nil}), do: false
defp encodable?(%{body: body}) when is_binary(body), do: false
defp encodable?(%{body: %Tesla.Multipart{}}), do: false
defp encodable?(_), do: true
@doc """
Decode response body as XML.
It is also used by `Tesla.Middleware.DecodeJson`.
"""
@spec decode(Tesla.Env.t(), keyword()) :: Tesla.Env.result()
def decode(env, opts) do
with true <- decodable?(env, opts),
{:ok, body} <- decode_body(env.body, opts) do
{:ok, %{env | body: body}}
else
false -> {:ok, env}
error -> error
end
end
defp decodable?(env, opts), do: decodable_body?(env) && decodable_content_type?(env, opts)
defp decodable_body?(env) do
(is_binary(env.body) && env.body != "") ||
(is_list(env.body) && env.body != []) ||
is_function(env.body) ||
is_struct(env.body, Stream)
end
defp decodable_content_type?(env, opts) do
case Tesla.get_header(env, "content-type") do
nil -> false
content_type -> Enum.any?(content_types(opts), &String.starts_with?(content_type, &1))
end
end
defp content_types(opts),
do: @default_content_types ++ Keyword.get(opts, :decode_content_types, [])
defp decode_body(body, opts) when is_struct(body, Stream) or is_function(body),
do: {:ok, decode_stream(body, opts)}
defp decode_body(body, opts), do: process(body, :decode, opts)
defp decode_stream(body, opts) do
Stream.map(body, fn chunk ->
case decode_body(chunk, opts) do
{:ok, item} -> item
_ -> chunk
end
end)
end
defp process(data, op, opts) do
case do_process(data, op, opts) do
{:ok, data} -> {:ok, data}
{:error, reason} -> {:error, {__MODULE__, op, reason}}
{:error, reason, _pos} -> {:error, {__MODULE__, op, reason}}
end
rescue
ex in Protocol.UndefinedError ->
{:error, {__MODULE__, op, ex}}
end
defp do_process(data, op, opts) do
# :encode/:decode
if fun = opts[op] do
fun.(data)
else
engine = Keyword.get(opts, :engine, @default_engine)
opts = Keyword.get(opts, :engine_opts, [])
adapter = Module.concat(Tesla.Middleware.XML.Adapters, engine)
apply(adapter, op, [data, opts])
end
end
end
defmodule Tesla.Middleware.DecodeXml do
@moduledoc """
Decodes response body as XML.
Only decodes the body if the `Content-Type` header suggests
that the body is XML.
"""
@behaviour Tesla.Middleware
@impl Tesla.Middleware
def call(env, next, opts) do
opts = opts || []
with {:ok, env} <- Tesla.run(env, next) do
Tesla.Middleware.XML.decode(env, opts)
end
end
end
defmodule Tesla.Middleware.EncodeXml do
@moduledoc """
Encodes request body as XML.
"""
@behaviour Tesla.Middleware
@impl Tesla.Middleware
def call(env, next, opts) do
opts = opts || []
with {:ok, env} <- Tesla.Middleware.XML.encode(env, opts) do
Tesla.run(env, next)
end
end
end