defmodule Soap do
@moduledoc """
The SOAP client for Elixir based on `HTTPoison` (for send requests) and `SweetXml` (for XML parsing).
Soap contains 5 main modules:
* `Soap.Wsdl` - Build wsdl components data map. Can parse raw wsdl file
from external url or local path. Wsdl which is prepared this module are
using for send requests.
* `Soap.Request` - Provides functionality for build and calling requests.
Contains Request.Headers and Soap.Params submodules for build headers and
build body with parameters validation respectively. This module is a
wrapper over HTTPoison. It send requests and handle them.
* `Soap.Response` - Handle soap response and handle them. It provides
functionality for parsing xml-like body and transform it to comfortable
structure. Structure for this module returns with necessary data after send
a request.
* `Soap.Xsd` - This module have same functionality as Soap.Wsdl module, but
only for Xsd-files. It allows to parse xsd files from external resources or
local path and convert it to map.
* `Soap.Type` - Provides a functionality for find and parse complex types
from raw xsd file. It uses in library for validation parameters when we
build request body.
The `Soap` module can be used to parse WSDL files:
```elixir
iex> Soap.init_model("https://git.io/vNCWd", :url)
{:ok, %{
complex_types: [...],
endpoint: "...",
messages: [...],
namespaces: %{...},
operations: [...],
schema_attributes: %{...},
soap_version: "x.x",
validation_types: %{...}
}
}
```
And send requests:
```elixir
iex> Soap.call(wsdl, action, params)
{:ok, %Soap.Response{}}
```
It's very common to use Soap in order to wrap APIs.
See `call/5` for more details on how to issue requests to soap services
"""
alias Soap.{Request, Response, Wsdl}
@doc """
Initialization of a WSDL model. Response a map of parsed data from file.
Returns `{:ok, wsdl}`.
## Parameters
- `path`: Path for wsdl file.
- `type`: Atom that represents the type of path for WSDL file. Can be `:file`
or `url`. Default: `:file`.
- `endpoint`: Endpoint to be used for the request. Defaults to the endpoint
specified in the WSDL file. Useful for (e.g.) sending a request to a mock
server during testing.
- `opts`: any options for `HTTPoison.Request` and the following parsing options:
* `:soap_version` - Specifies SOAP version for parsing.
* `:allow_empty_soap_actions` - Allows SOAP operations with an empty
`soapAction` attribute. This may be required for APIs that do not set a
`soapAction` for each operation.
* `:skip_type_imports` - Prevents fetching external XSDs for importing types.
## Examples
iex> {:ok, wsdl} = Soap.init_model("https://git.io/vNCWd", :url)
{:ok, %{...}}
"""
@spec init_model(String.t(), :file | :url, list()) :: {:ok, map()}
def init_model(path, type \\ :file, opts \\ [])
def init_model(path, :file, opts), do: Wsdl.parse_from_file(path, opts)
def init_model(path, :url, opts), do: Wsdl.parse_from_url(path, opts)
@doc """
Send a request to the SOAP server based on the passed WSDL file, action and parameters.
Returns `{:ok, %Soap.Response{}}` if the request is successful, `{:error, reason}` otherwise.
## Parameters
- `wsdl`: Wsdl model from `Soap.init_model/2` function.
- `action`: Soap action to be called. Use `Soap.operations/1` to get a list of available actions
- `params`: Parameters to build the body of a SOAP request.
- `headers`: Custom request headers.
- `opts`: HTTPoison options.
## Examples
iex> Soap.call(wsdl, action, params)
{:ok, %Soap.Response{}}
"""
@spec call(wsdl :: map(), operation :: String.t(), params :: map(), headers :: any(), opts :: any()) :: any()
def call(wsdl, operation, params, headers \\ [], opts \\ []) do
wsdl
|> validate_operation(operation)
|> Request.call(operation, params, headers, opts)
|> handle_response
end
@doc """
Returns a list of available actions of the passed WSDL.
## Parameters
- `wsdl`: Wsdl model from `Soap.init_model/2` function.
## Examples
iex> {:ok, wsdl} = Soap.init_model("https://git.io/vNCWd", :url)
iex> Soap.operations(wsdl)
["SendMessage", "SendMessageMultipleRecipients"]
"""
@spec operations(map()) :: nonempty_list(String.t())
def operations(wsdl) do
wsdl.operations
end
defp handle_response(
{:ok, %HTTPoison.Response{body: body, headers: headers, request_url: request_url, status_code: status_code}}
) do
{:ok, %Response{body: body, headers: headers, request_url: request_url, status_code: status_code}}
end
defp handle_response({:error, %HTTPoison.Error{reason: reason}}) do
{:error, reason}
end
defp validate_operation(wsdl, operation) do
case valid_operation?(wsdl, operation) do
false -> raise OperationError, operation
true -> wsdl
end
end
defp valid_operation?(wsdl, operation) do
Enum.any?(wsdl[:operations], &(&1[:name] == operation))
end
end