lib/soap/request/params.ex

defmodule Soap.Request.Params do
  @moduledoc """
  Documentation for Soap.Request.Options.
  """
  import XmlBuilder, only: [element: 3, document: 1, generate: 2]

  @schema_types %{
    "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
    "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
  }
  @soap_version_namespaces %{
    "1.1" => "http://schemas.xmlsoap.org/soap/envelope/",
    "1.2" => "http://www.w3.org/2003/05/soap-envelope"
  }
  @date_type_regex "[0-9]{4}-[0-9]{2}-[0-9]{2}"
  @date_time_type_regex "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"

  @doc """
  Parsing parameters map and generate body xml by given soap action name and body params(Map).

  Returns XML-like string.
  """

  @spec build_body(wsdl :: map(), operation :: String.t() | atom(), params :: map() | tuple(), headers :: map()) ::
          String.t()
  def build_body(wsdl, operation, params, headers) do
    with {:ok, body} <- build_soap_body(wsdl, operation, params),
         {:ok, header} <- build_soap_header(wsdl, operation, headers) do
      [header, body]
      |> add_envelope_tag_wrapper(wsdl, operation)
      |> document
      |> generate(format: :none)
      |> String.replace(["\n", "\t"], "")
    else
      {:error, message} -> message
    end
  end

  @spec validate_params(params :: any(), wsdl :: map(), operation :: String.t()) :: any()
  def validate_params(params, _wsdl, _operation) when is_binary(params), do: params

  def validate_params({_tag, _attrs, _nested} = param, wsdl, operation) do
    case validate_param(param, wsdl, operation) do
      nil -> param
      error -> {:error, error}
    end
  end

  def validate_params(params, wsdl, operation) do
    errors =
      params
      |> Enum.map(&validate_param(&1, wsdl, operation))

    case Enum.any?(errors) do
      true ->
        {:error, Enum.reject(errors, &is_nil/1)}

      _ ->
        params
    end
  end

  @spec validate_param(param :: tuple(), wsdl :: map(), operation :: String.t()) :: String.t() | nil
  defp validate_param([param], wsdl, operation) do
    validate_param(param, wsdl, operation)
  end

  defp validate_param(param, wsdl, operation) do
    {k, _, v} = param

    case val_map = wsdl.validation_types[String.downcase(operation)] do
      nil ->
        nil

      _ ->
        if Map.has_key?(val_map, k) do
          validate_param_attributes(val_map, k, v)
        else
          "Invalid SOAP message:Invalid content was found starting with element '#{k}'. One of {#{Enum.join(Map.keys(val_map), ", ")}} is expected."
        end
    end
  end

  @spec validate_param_attributes(val_map :: map(), k :: String.t(), v :: String.t()) :: String.t() | nil
  defp validate_param_attributes(val_map, k, v) do
    attributes = val_map[k]
    [_, type] = String.split(attributes.type, ":")

    validate_type(k, v, type)
  end

  defp validate_type(_k, v, "string") when is_binary(v), do: nil
  defp validate_type(k, _v, type = "string"), do: type_error_message(k, type)

  defp validate_type(_k, v, "decimal") when is_number(v), do: nil
  defp validate_type(k, _v, type = "decimal"), do: type_error_message(k, type)

  defp validate_type(k, v, "date") when is_binary(v) do
    case Regex.match?(~r/#{@date_type_regex}/, v) do
      true -> nil
      _ -> format_error_message(k, @date_type_regex)
    end
  end

  defp validate_type(k, _v, type = "date"), do: type_error_message(k, type)

  defp validate_type(k, v, "dateTime") when is_binary(v) do
    case Regex.match?(~r/#{@date_time_type_regex}/, v) do
      true -> nil
      _ -> format_error_message(k, @date_time_type_regex)
    end

    nil
  end

  defp validate_type(k, _v, type = "dateTime"), do: type_error_message(k, type)

  defp build_soap_body(wsdl, operation, params) do
    case params |> construct_xml_request_body |> validate_params(wsdl, operation) do
      {:error, messages} ->
        {:error, messages}

      validated_params ->
        body =
          validated_params
          |> add_action_tag_wrapper(wsdl, operation)
          |> add_body_tag_wrapper

        {:ok, body}
    end
  end

  defp build_soap_header(wsdl, operation, headers) do
    case headers |> construct_xml_request_header do
      {:error, messages} ->
        {:error, messages}

      validated_params ->
        body =
          validated_params
          |> add_header_part_tag_wrapper(wsdl, operation)
          |> add_header_tag_wrapper

        {:ok, body}
    end
  end

  defp type_error_message(k, type) do
    "Element #{k} has wrong type. Expects #{type} type."
  end

  defp format_error_message(k, regex) do
    "Element #{k} has wrong format. Expects #{regex} format."
  end

  @spec construct_xml_request_body(params :: map() | list()) :: list()
  defp construct_xml_request_body(params) when is_map(params) or is_list(params) do
    params |> Enum.map(&construct_xml_request_body/1)
  end

  @spec construct_xml_request_body(params :: tuple()) :: tuple()
  defp construct_xml_request_body({tag, attrs, nested}) do
    [{to_string(tag), attrs, construct_xml_request_body(nested)}]
  end

  defp construct_xml_request_body(params) when is_tuple(params) do
    params
    |> Tuple.to_list()
    |> Enum.map(&construct_xml_request_body/1)
    |> insert_tag_parameters
    |> List.to_tuple()
  end

  @spec construct_xml_request_body(params :: String.t() | atom() | number()) :: String.t()
  defp construct_xml_request_body(params) when is_atom(params), do: params |> to_string()
  defp construct_xml_request_body(params) when is_binary(params) or is_number(params), do: params

  @spec construct_xml_request_header(params :: map() | list()) :: list()
  defp construct_xml_request_header(params) when is_map(params) or is_list(params) do
    params |> Enum.map(&construct_xml_request_header/1)
  end

  @spec construct_xml_request_header(params :: tuple()) :: tuple()
  defp construct_xml_request_header(params) when is_tuple(params) do
    params
    |> Tuple.to_list()
    |> Enum.map(&construct_xml_request_header/1)
    |> insert_tag_parameters
    |> List.to_tuple()
  end

  @spec construct_xml_request_header(params :: String.t() | atom() | number()) :: String.t()
  defp construct_xml_request_header(params) when is_atom(params) or is_number(params), do: params |> to_string
  defp construct_xml_request_header(params) when is_binary(params), do: params

  @spec insert_tag_parameters(params :: list()) :: list()
  defp insert_tag_parameters(params) when is_list(params), do: params |> List.insert_at(1, nil)

  @spec add_action_tag_wrapper(list(), map(), String.t()) :: list()
  defp add_action_tag_wrapper(body, wsdl, operation) do
    action_tag_attributes = handle_element_form_default(wsdl[:schema_attributes])

    action_tag =
      wsdl
      |> get_action_with_namespace(operation)
      |> prepare_action_tag(operation)

    [element(action_tag, action_tag_attributes, body)]
  end

  @spec add_header_part_tag_wrapper(list(), map(), String.t()) :: list()
  defp add_header_part_tag_wrapper(body, wsdl, operation) do
    action_tag_attributes = handle_element_form_default(wsdl[:schema_attributes])

    case get_header_with_namespace(wsdl, operation) do
      nil ->
        nil

      action_tag ->
        [element(action_tag, action_tag_attributes, body)]
    end
  end

  defp handle_element_form_default(%{target_namespace: ns, element_form_default: "qualified"}), do: %{xmlns: ns}
  defp handle_element_form_default(_schema_attributes), do: %{}

  defp prepare_action_tag("", operation), do: operation
  defp prepare_action_tag(action_tag, _operation), do: action_tag

  @spec get_action_with_namespace(wsdl :: map(), operation :: String.t()) :: String.t()
  defp get_action_with_namespace(wsdl, operation) do
    case wsdl[:complex_types] do
      [] ->
        ""

      _ ->
        wsdl[:complex_types]
        |> Enum.find(fn x -> x[:name] == operation end)
        |> handle_action_extractor_result(wsdl, operation)
    end
  end

  @spec get_header_with_namespace(wsdl :: map(), operation :: String.t()) :: String.t() | nil
  defp get_header_with_namespace(wsdl, operation) do
    with %{input: %{header: %{message: message, part: part}}} <-
           Enum.find(wsdl[:operations], &(&1[:name] == operation)),
         %{name: name} <- get_message_part(wsdl, message, part) do
      name
    else
      _ -> nil
    end
  end

  defp get_message_part(wsdl, message, part) do
    wsdl[:messages]
    |> Enum.find(&("tns:#{&1[:name]}" == message))
    |> Map.get(:parts)
    |> Enum.find(&(&1[:name] == part))
  end

  defp handle_action_extractor_result(nil, wsdl, operation) do
    wsdl[:complex_types]
    |> Enum.find(fn x -> Macro.camelize(x[:name]) == operation end)
    |> Map.get(:type)
  end

  defp handle_action_extractor_result(result, _wsdl, _operation), do: Map.get(result, :type)

  @spec get_action_namespace(wsdl :: map(), operation :: String.t()) :: String.t()
  defp get_action_namespace(wsdl, operation) do
    wsdl
    |> get_action_with_namespace(operation)
    |> String.split(":")
    |> List.first()
  end

  @spec add_body_tag_wrapper(list()) :: list()
  defp add_body_tag_wrapper(body), do: [element(:"#{env_namespace()}:Body", nil, body)]

  @spec add_header_tag_wrapper(list()) :: list()
  defp add_header_tag_wrapper(body), do: [element(:"#{env_namespace()}:Header", nil, body)]

  @spec add_envelope_tag_wrapper(body :: any(), wsdl :: map(), operation :: String.t()) :: any()
  defp add_envelope_tag_wrapper(body, wsdl, operation) do
    envelop_attributes =
      @schema_types
      |> Map.merge(build_soap_version_attribute(wsdl))
      |> Map.merge(build_action_attribute(wsdl, operation))
      |> Map.merge(custom_namespaces())

    [element(:"#{env_namespace()}:Envelope", envelop_attributes, body)]
  end

  @spec build_soap_version_attribute(map()) :: map()
  defp build_soap_version_attribute(wsdl) do
    soap_version = wsdl |> soap_version() |> to_string
    %{"xmlns:#{env_namespace()}" => @soap_version_namespaces[soap_version]}
  end

  @spec build_action_attribute(map(), String.t()) :: map()
  defp build_action_attribute(wsdl, operation) do
    action_attribute_namespace = get_action_namespace(wsdl, operation)
    action_attribute_value = wsdl[:namespaces][action_attribute_namespace][:value]
    prepare_action_attribute(action_attribute_namespace, action_attribute_value)
  end

  defp prepare_action_attribute(_action_attribute_namespace, nil), do: %{}

  defp prepare_action_attribute(action_attribute_namespace, action_attribute_value) do
    %{"xmlns:#{action_attribute_namespace}" => action_attribute_value}
  end

  defp soap_version(wsdl) do
    Map.get(wsdl, :soap_version, Application.fetch_env!(:soap, :globals)[:version])
  end

  defp env_namespace, do: Application.fetch_env!(:soap, :globals)[:env_namespace] || :env
  defp custom_namespaces, do: Application.fetch_env!(:soap, :globals)[:custom_namespaces] || %{}
end