From 912ab3c83c4160ac62d2ff6f99e738e3c820ea10 Mon Sep 17 00:00:00 2001
From: Brian Underwood <public@brian-underwood.codes>
Date: Tue, 3 May 2022 11:23:47 +0200
Subject: [PATCH] Add the ability to have safe params

This will pass a {:safe, _} tuple down into XmlBuilder where we can avoid escaping strings that don't need to be escaped
---
 README.md                                     |  8 ++++
 lib/soap/request/params.ex                    | 39 +++++++++----------
 mix.exs                                       |  2 +-
 .../xml/send_service/MarkedAsSafeRequest.xml  |  1 +
 test/soap/request/params_test.exs             | 14 +++++++
 5 files changed, 42 insertions(+), 22 deletions(-)
 create mode 100644 test/fixtures/xml/send_service/MarkedAsSafeRequest.xml

diff --git a/README.md b/README.md
index bc436ea..3005779 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,14 @@ iex> {:ok, response} = Soap.call(wsdl, action, params)
  }}
 ```
 
+Safe strings:
+
+You might have strings that you know are safe where you'd like to skip the escaping step in generating the XML (for example if you have a lot of Base64 encoded data).  For this situation you can mark data as "safe":
+
+```elixir
+params = %{data: {:__safe, data}}
+```
+
 Parse response:
 
 ```elixir
diff --git a/lib/soap/request/params.ex b/lib/soap/request/params.ex
index 7f10aa8..941dccd 100644
--- a/lib/soap/request/params.ex
+++ b/lib/soap/request/params.ex
@@ -90,6 +90,7 @@ defmodule Soap.Request.Params do
     validate_type(k, v, type)
   end
 
+  defp validate_type(k, {:safe, v}, type), do: validate_type(k, v, type)
   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)
 
@@ -159,44 +160,40 @@ defmodule Soap.Request.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)}]
+  defp construct_xml_request_body({:__safe, value}), do: {:safe, value}
+
+  defp construct_xml_request_body({tag, value}) do
+    {to_string(tag), nil, construct_xml_request_body(value)}
   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()
+  @spec construct_xml_request_body({term(), term()}) :: {term(), term()}
+  defp construct_xml_request_body({tag, attrs, nested}) do
+    [{to_string(tag), attrs, construct_xml_request_body(nested)}]
   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
 
+  # defp construct_xml_request_header({:__safe, value}), do: {:safe, value}
+
+  @spec construct_xml_request_header({term(), term()}) :: {term(), term()}
+  defp construct_xml_request_header({tag, value}) do
+    {to_string(tag), nil, construct_xml_request_header(value)}
+  end
+
+  @spec insert_tag_parameters(params :: list()) :: list()
+  defp insert_tag_parameters(params) when is_list(params), do: params |> List.insert_at(1, nil)
+
   @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])
diff --git a/mix.exs b/mix.exs
index 58ef0a4..06ee65d 100644
--- a/mix.exs
+++ b/mix.exs
@@ -2,7 +2,7 @@ defmodule Soap.MixProject do
   use Mix.Project
 
   @source_url "https://github.com/elixir-soap/soap"
-  @version "1.1.0"
+  @version "1.1.1"
 
   def project do
     [
diff --git a/test/fixtures/xml/send_service/MarkedAsSafeRequest.xml b/test/fixtures/xml/send_service/MarkedAsSafeRequest.xml
new file mode 100644
index 0000000..06ee718
--- /dev/null
+++ b/test/fixtures/xml/send_service/MarkedAsSafeRequest.xml
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="com.esendex.ems.soapinterface" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><env:Header/><env:Body><tns:sendMessage xmlns="com.esendex.ems.soapinterface"><type>TY&PE</type></tns:sendMessage></env:Body></env:Envelope>
diff --git a/test/soap/request/params_test.exs b/test/soap/request/params_test.exs
index 636330c..0e8a22c 100644
--- a/test/soap/request/params_test.exs
+++ b/test/soap/request/params_test.exs
@@ -56,6 +56,20 @@ defmodule Soap.Request.ParamsTest do
     assert function_result == xml_body
   end
 
+  test "values can be marked as safe" do
+    xml_body =
+      Fixtures.load_xml("send_service/MarkedAsSafeRequest.xml")
+      |> String.replace("WSPB", "123")
+
+    parameters = %{type: {:__safe, "TY&PE"}}
+    {_, wsdl} = Wsdl.parse_from_file(@wsdl_path)
+    # This will be an invalid XML file, but we need to test that XmlBuilder
+    # sends the value straight through
+    function_result = Params.build_body(wsdl, @operation, parameters, nil)
+
+    assert function_result == xml_body
+  end
+
   test "#build_body returns wrong date format errors" do
     parameters = %{"date" => "09:00:00"}
     {_, wsdl} = Wsdl.parse_from_file(@wsdl_path)