defmodule Soap.Wsdl do
@moduledoc """
Provides functions for parsing wsdl file.
"""
@soap_version_namespaces %{
"1.1" => :"http://schemas.xmlsoap.org/wsdl/soap/",
"1.2" => :"http://schemas.xmlsoap.org/wsdl/soap12/"
}
import SweetXml, except: [parse: 1, parse: 2]
alias Soap.{Request, Type, Xsd}
@spec parse_from_file(String.t()) :: {:ok, map()}
def parse_from_file(path, opts \\ []) do
{:ok, wsdl} = File.read(path)
parse(wsdl, path, opts)
end
@spec parse_from_url(String.t()) :: {:ok, map()}
def parse_from_url(path, opts \\ []) do
request_opts = Keyword.merge([follow_redirect: true, max_redirect: 5], opts)
%HTTPoison.Response{body: wsdl} = Request.get_http_client().get!(path, [], request_opts)
parse(wsdl, path, opts)
end
@spec parse(String.t(), String.t(), Keyword.t()) :: {:ok, map()}
def parse(wsdl, file_path, opts \\ []) do
wsdl = SweetXml.parse(wsdl)
protocol_namespace = get_protocol_namespace(wsdl)
soap_namespace = get_soap_namespace(wsdl, opts)
schema_namespace = get_schema_namespace(wsdl)
endpoint = Keyword.get(opts, :endpoint, get_endpoint(wsdl, protocol_namespace, soap_namespace))
parsed_response = %{
namespaces: get_namespaces(wsdl, schema_namespace, protocol_namespace),
endpoint: endpoint,
complex_types: get_complex_types(wsdl, schema_namespace, protocol_namespace),
operations: get_operations(wsdl, protocol_namespace, soap_namespace, opts),
schema_attributes: get_schema_attributes(wsdl),
validation_types: get_validation_types(wsdl, file_path, protocol_namespace, schema_namespace, endpoint, opts),
soap_version: soap_version(opts),
messages: get_messages(wsdl, protocol_namespace)
}
{:ok, parsed_response}
end
@spec get_schema_namespace(String.t()) :: String.t()
defp get_schema_namespace(wsdl) do
{_, _, _, schema_namespace, _} =
wsdl
|> xpath(~x"//namespace::*"l)
|> Enum.find(fn {_, _, _, _, x} -> x == :"http://www.w3.org/2001/XMLSchema" end)
schema_namespace
end
@spec get_namespaces(String.t(), String.t(), String.t()) :: map()
defp get_namespaces(wsdl, schema_namespace, protocol_ns) do
wsdl
|> xpath(~x"//#{ns("definitions", protocol_ns)}/namespace::*"l)
|> Enum.into(%{}, &get_namespace(&1, wsdl, schema_namespace, protocol_ns))
end
@spec get_namespace(tuple(), String.t(), String.t(), String.t()) :: tuple()
defp get_namespace(namespaces_node, wsdl, schema_namespace, protocol_ns) do
{_, _, _, key, value} = namespaces_node
string_key = key |> to_string
value = Atom.to_string(value)
cond do
xpath(wsdl, ~x"//#{ns("definitions", protocol_ns)}[@targetNamespace='#{value}']") ->
{string_key, %{value: value, type: :wsdl}}
xpath(
wsdl,
~x"//#{ns("types", protocol_ns)}/#{ns("schema", schema_namespace)}/#{ns("import", schema_namespace)}[@namespace='#{value}']"
) ->
{string_key, %{value: value, type: :xsd}}
true ->
{string_key, %{value: value, type: :soap}}
end
end
@spec get_endpoint(String.t(), String.t(), String.t()) :: String.t()
def get_endpoint(wsdl, protocol_ns, soap_ns) do
wsdl
|> xpath(
~x"//#{ns("definitions", protocol_ns)}/#{ns("service", protocol_ns)}/#{ns("port", protocol_ns)}/#{ns("address", soap_ns)}/@location"s
)
end
@spec get_complex_types(String.t(), String.t(), String.t()) :: list()
defp get_complex_types(wsdl, namespace, protocol_ns) do
xpath(
wsdl,
~x"//#{ns("types", protocol_ns)}/#{ns("schema", namespace)}/#{ns("element", namespace)}"l,
name: ~x"./@name"s,
type: ~x"./@type"s
)
end
@spec get_validation_types(String.t(), String.t(), String.t(), String.t(), String.t(), keyword()) :: map()
def get_validation_types(wsdl, file_path, protocol_ns, schema_ns, endpoint, opts \\ []) do
Map.merge(
Type.get_complex_types(
wsdl,
"//#{ns("types", protocol_ns)}/#{ns("schema", schema_ns)}/#{ns("complexType", schema_ns)}"
),
wsdl
|> get_full_paths(file_path, protocol_ns, schema_ns, endpoint)
|> get_imported_types(opts)
|> Enum.reduce(%{}, &Map.merge(&2, &1))
)
end
@spec get_schema_imports(String.t(), String.t(), String.t()) :: list()
def get_schema_imports(wsdl, protocol_ns, schema_ns) do
xpath(
wsdl,
~x"//#{ns("types", protocol_ns)}/#{ns("schema", schema_ns)}/#{ns("import", schema_ns)}"l,
schema_location: ~x"./@schemaLocation"s
)
end
@spec get_full_paths(String.t(), String.t(), String.t(), String.t(), String.t()) :: list(String.t())
defp get_full_paths(wsdl, path, protocol_ns, schema_namespace, endpoint) do
wsdl
|> get_schema_imports(protocol_ns, schema_namespace)
|> Enum.map(&resolve_schema_imports(path, &1.schema_location, endpoint))
end
@spec resolve_schema_imports(String.t(), String.t(), String.t()) :: String.t()
defp resolve_schema_imports(path, location, endpoint) do
case URI.parse(location) do
%URI{scheme: nil} ->
case URI.parse(path) do
%URI{scheme: nil} -> path |> Path.dirname() |> Path.join(location)
_ -> Path.join(endpoint, location)
end
_ ->
location
end
end
@spec get_imported_types(list(), keyword()) :: list(map())
defp get_imported_types(xsd_paths, opts) do
opts
|> Keyword.get(:skip_type_imports, false)
|> case do
true -> %{}
_ -> do_get_imported_types(xsd_paths, opts)
end
end
@spec do_get_imported_types(list(), keyword()) :: list(map())
defp do_get_imported_types(xsd_paths, opts) do
xsd_paths
|> Enum.map(fn xsd_path ->
case Xsd.parse(xsd_path, opts) do
{:ok, xsd} -> xsd.complex_types
_ -> %{}
end
end)
end
defp get_operations(wsdl, protocol_ns, soap_ns, opts) do
wsdl
|> xpath(~x"//#{ns("definitions", protocol_ns)}/#{ns("binding", protocol_ns)}/#{ns("operation", protocol_ns)}"l)
|> Enum.map(fn node ->
node
|> xpath(~x".", name: ~x"./@name"s, soap_action: ~x"./#{ns("operation", soap_ns)}/@soapAction"s)
|> Map.put(:input, get_operation_input(node, protocol_ns, soap_ns))
end)
|> Enum.reject(fn x -> x[:soap_action] == "" && !opts[:allow_empty_soap_actions] end)
end
defp get_operation_input(element, protocol_ns, soap_ns) do
case xpath(element, ~x"./#{ns("input", protocol_ns)}/#{ns("header", soap_ns)}") do
nil ->
%{
body: nil,
header: nil
}
header_node ->
%{
body: nil,
header: xpath(header_node, ~x".", message: ~x"./@message"s, part: ~x"./@part"s)
}
end
end
defp get_messages(wsdl, protocol_ns) do
wsdl
|> xpath(~x"//#{ns("definitions", protocol_ns)}/#{ns("message", protocol_ns)}"l)
|> Enum.map(fn node ->
node
|> xpath(~x".", name: ~x"./@name"s)
|> Map.put(:parts, get_message_parts(node, protocol_ns))
end)
end
defp get_message_parts(element, protocol_ns) do
xpath(element, ~x"./#{ns("part", protocol_ns)}"l, name: ~x"./@name"s, element: ~x"./@element"s)
end
@spec get_protocol_namespace(String.t()) :: String.t()
defp get_protocol_namespace(wsdl) do
wsdl
|> xpath(~x"//namespace::*"l)
|> Enum.find(fn {_, _, _, _, url} -> url == :"http://schemas.xmlsoap.org/wsdl/" end)
|> elem(3)
end
@spec get_soap_namespace(String.t(), list()) :: String.t()
defp get_soap_namespace(wsdl, opts) when is_list(opts) do
version = soap_version(opts)
namespace = @soap_version_namespaces[version]
wsdl
|> xpath(~x"//namespace::*"l)
|> Enum.find(fn {_, _, _, _, url} -> url == namespace end)
|> elem(3)
end
@spec get_schema_attributes(String.t()) :: map()
defp get_schema_attributes(wsdl) do
case xpath(wsdl, ~x"//*[local-name() = 'schema']") do
nil ->
%{}
schema ->
xpath(schema, ~x".",
target_namespace: ~x"./@targetNamespace"s,
element_form_default: ~x"./@elementFormDefault"s
)
end
end
defp soap_version, do: Application.fetch_env!(:soap, :globals)[:version]
defp soap_version(opts) when is_list(opts), do: Keyword.get(opts, :soap_version, soap_version())
defp ns(name, []), do: "#{name}"
defp ns(name, namespace), do: "#{namespace}:#{name}"
end