From 7915fd769d991b2a5b79a0d0f5de55b4f476d43b Mon Sep 17 00:00:00 2001 From: Michal Darda Date: Mon, 23 Mar 2020 16:10:04 +0100 Subject: [PATCH 1/4] Add Tesla middleware --- .gitignore | 2 + README.md | 82 ++++++--- lib/xml_rpc/tesla/middleware.ex | 158 +++++++++++++++++ mix.exs | 45 ++--- mix.lock | 16 +- test/xmlrpc/tesla/middleware_test.exs | 233 ++++++++++++++++++++++++++ 6 files changed, 490 insertions(+), 46 deletions(-) create mode 100644 lib/xml_rpc/tesla/middleware.ex create mode 100644 test/xmlrpc/tesla/middleware_test.exs diff --git a/.gitignore b/.gitignore index 26d2413..ef59139 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ erl_crash.dump /bench/snapshots /bench/graphs + +.devcontainer \ No newline at end of file diff --git a/README.md b/README.md index d85e9e6..8624aa7 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ the risk that a malicious client can exhaust out atom space and crash the vm. Add XML-RPC to your mix dependencies - def deps do - [{:xmlrpc, "~> 1.0"}] - end +```elixir +def deps do + [{:xmlrpc, "~> 1.0"}] +end +``` Then run `mix deps.get` and `mix deps.compile`. @@ -62,48 +64,88 @@ If you want a input to be treated as an error then pass The XML-RPC api consists of a call to a remote url, passing a "method_name" and a number of parameters. - %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} +```elixir +%XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} +``` The response is either "failure" and a `fault_code` and `fault_string`, or a response which consists of a single parameter (use a struct/array to pass back multiple values) - %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} +```elixir +%XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} +``` - %XMLRPC.MethodResponse{param: 30} +```elixir +%XMLRPC.MethodResponse{param: 30} +``` To encode/decode to xml use `XMLRPC.encode/2` or `XMLRPC.decode/2` ## Examples +### Client using Tesla + +[Tesla](https://github.com/teamon/tesla) can be used to talk to the remote API. + +There is dedicated Middleware available [here](lib/xml_rpc/tesla/middleware.ex). + +```elixir +defmodule Client do + use Tesla + + plug XMLRPC.Tesla.Middleware + + def sumprod(a, b) do + post("http://www.advogato.org/XMLRPC", %XMLRPC.MethodCall(method_name: "test.sumprod", params: [a, b])) + end +end +``` + +Your request body will be automatically converted to `%XMLRPC.MethodCall` struct and your response will be +automatically marshalled to `%XMLRPC.MethodResponse`. + +You can use `XMLRPC.Tesla.Middleware.Encode` or `XMLRPC.Tesla.Middleware.Decode` middlewares if you only +need encode/decode option. By default only 2xx responses are parsed but that is configurable. + +```elixir +plug XMLRPC.Tesla.Middleware, decodable_status: fn status -> status in 200..299 +``` + +You can see all options available in the moduledoc [here](lib/xml_rpc/tesla/middleware.ex). + ### Client using HTTPoison [HTTPoison](https://github.com/edgurgel/httpoison) can be used to talk to the remote API. To encode the body we can simply call `XMLRPC.encode/2`, and then decode the response with `XMLRPC.decode/2` - request_body = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} - |> XMLRPC.encode! - "test.sumprod23" +```elixir +request_body = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} + |> XMLRPC.encode! +"test.sumprod23" - # Now use HTTPoison to call your RPC - response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request_body).body +# Now use HTTPoison to call your RPC +response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request_body).body - # eg - response = "56" - |> XMLRPC.decode - {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} +# eg +response = "56" + |> XMLRPC.decode +{:ok, %XMLRPC.MethodResponse{param: [5, 6]}} +``` See the [HTTPoison docs](https://github.com/edgurgel/httpoison#wrapping-httpoisonbase) for more details, but you can also wrap the base API and have HTTPoison automatically do your encoding and decoding. In this way its very simple to build higher level APIs - defmodule XMLRPC do - use HTTPoison.Base +```elixir +defmodule XMLRPC do + use HTTPoison.Base - def process_request_body(body), do: XMLRPC.encode(body) - def process_response_body(body), do: XMLRPC.decode(body) - end + def process_request_body(body), do: XMLRPC.encode(body) + def process_response_body(body), do: XMLRPC.decode(body) +end +``` iex> request = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} iex> response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request).body diff --git a/lib/xml_rpc/tesla/middleware.ex b/lib/xml_rpc/tesla/middleware.ex new file mode 100644 index 0000000..c57ec86 --- /dev/null +++ b/lib/xml_rpc/tesla/middleware.ex @@ -0,0 +1,158 @@ +if Code.ensure_loaded?(Tesla) do + defmodule XMLRPC.Tesla.Middleware do + @moduledoc """ + Tesla Middleware that encodes and decodes XMLRPC + + ## Example usage + ``` + defmodule MyClient do + use Tesla + plug XMLRPC.Tesla.Middleware + # or custom functions allowing to post process + plug Tesla.Middleware.JSON, decode: &XMLRPC.decode/1, encode: &XMLRPC.encode/1 + end + ``` + ## Options + - `:decode` - decoding function + - `:encode` - encoding function + - `:encode_content_type` - content-type to be used in request header + - `:engine_opts` - optional engine (XMLRPC) options + - `:decode_content_types` - list of additional decodable content-types + - `:decodable_status` - status function to be decodable (default 200..299) + """ + + @behaviour Tesla.Middleware + + @default_encode_content_type "application/xml" + @default_content_types ["application/xml"] + def default_decodable_status(status), do: status in 200..299 + + @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 """ + Encodes request body as XMLRPC. + It is used by `XMLRPC.Tesla.Middleware.Encode`. + """ + def encode(env, opts) do + with true <- encodable?(env, opts), + {: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 + + @doc """ + Decodes request body as XMLRPC. + It is used by `XMLRPC.Tesla.Middleware.Decode`. + """ + 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 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 encodable?(%{body: nil}, _opts), do: false + defp encodable?(%{body: body}, _opts) when is_binary(body), do: false + defp encodable?(%{body: %XMLRPC.MethodCall{}}, _opts), do: true + + defp encodable?(_env, _opts), do: false + + defp decodable?(env, opts), + do: + decodable_content_type?(env, opts) && + decodable_status?(env, opts) && + decodable_body?(env) + + def decodable_status?(env, opts) do + f = Keyword.get(opts, :decodable_status, &__MODULE__.default_decodable_status/1) + f.(env.status) + 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 decodable_body?(env) do + is_binary(env.body) && env.body != "" + end + + defp decode_body(body, opts), do: process(body, :decode, opts) + + 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 + if f = opts[op] do + f.(data) + else + opts = Keyword.get(opts, :engine_opts, []) + apply(XMLRPC, op, [data, opts]) + end + end + end + + defmodule XMLRPC.Tesla.Middleware.Decode do + @moduledoc """ + Middleware that only decodes XMLRPC + """ + def call(env, next, opts) do + opts = opts || [] + + with {:ok, env} <- Tesla.run(env, next) do + XMLRPC.Tesla.Middleware.decode(env, opts) + end + end + end + + defmodule XMLRPC.Tesla.Middleware.Encode do + @moduledoc """ + Middleware that only decodes XMLRPC + """ + def call(env, next, opts) do + opts = opts || [] + + with {:ok, env} <- XMLRPC.Tesla.Middleware.encode(env, opts) do + Tesla.run(env, next) + end + end + end +end diff --git a/mix.exs b/mix.exs index e124062..534d3fb 100644 --- a/mix.exs +++ b/mix.exs @@ -1,17 +1,20 @@ -defmodule XmlRpc.Mixfile do +defmodule XMLRPC.Mixfile do use Mix.Project def project do - [app: :xmlrpc, - version: "1.4.0", - elixir: "~> 1.4", - name: "XMLRPC", - description: "XML-RPC encoder/decder for Elixir. Supports all valid datatypes. Input (ie untrusted) is parsed with erlsom against an xml-schema for security.", - source_url: "https://github.com/ewildgoose/elixir-xml_rpc", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps(), - package: package()] + [ + app: :xmlrpc, + version: "1.5.0", + elixir: "~> 1.4", + name: "XMLRPC", + description: + "XML-RPC encoder/decder for Elixir. Supports all valid datatypes. Input (ie untrusted) is parsed with erlsom against an xml-schema for security.", + source_url: "https://github.com/ewildgoose/elixir-xml_rpc", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + package: package() + ] end # Configuration for the OTP application @@ -31,17 +34,21 @@ defmodule XmlRpc.Mixfile do # # Type `mix help deps` for more examples and options defp deps do - [ {:earmark, "~> 1.0", only: :docs}, - {:ex_doc, "~> 0.14", only: :docs}, - {:erlsom, "~> 1.4"}, - {:decimal, "~> 1.0"}, + [ + {:earmark, "~> 1.0", only: :docs}, + {:ex_doc, "~> 0.14", only: :docs}, + {:erlsom, "~> 1.4"}, + {:decimal, "~> 1.0"}, + {:tesla, "~> 1.3.2", optional: true} ] end defp package do - [files: ~w(lib mix.exs README.md LICENSE), - maintainers: ["Ed Wildgoose"], - licenses: ["Apache 2.0"], - links: %{"GitHub" => "https://github.com/ewildgoose/elixir-xml_rpc"}] + [ + files: ~w(lib mix.exs README.md LICENSE), + maintainers: ["Ed Wildgoose"], + licenses: ["Apache 2.0"], + links: %{"GitHub" => "https://github.com/ewildgoose/elixir-xml_rpc"} + ] end end diff --git a/mix.lock b/mix.lock index 1d6755d..910bea4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,15 +1,17 @@ %{ "calendar": {:hex, :calendar, "0.6.7"}, - "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, - "erlsom": {:hex, :erlsom, "1.4.2", "5cddb82fb512f406f61162e511ae86582f824f0dccda788378b18a00d89c1b3f", [:rebar3], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm", "52694ef56e60108e5012f8af9673874c66ed58ac1c4fae9b5b7ded31786663f5"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, + "erlsom": {:hex, :erlsom, "1.4.2", "5cddb82fb512f406f61162e511ae86582f824f0dccda788378b18a00d89c1b3f", [:rebar3], [], "hexpm", "ac989e850a5a4c1641694f77506804710315f3d1193c977a36b223a32859edd3"}, + "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"}, "hackney": {:hex, :hackney, "1.1.0"}, "httpoison": {:hex, :httpoison, "0.7.0"}, "idna": {:hex, :idna, "1.0.2"}, - "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, + "tesla": {:hex, :tesla, "1.3.2", "deb92c5c9ce35e747a395ba413ca78593a4f75bf0e1545630ee2e3d34264021e", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7567704c4790e21bd9a961b56d0b6a988ff68cc4dacfe6b2106e258da1d5cdda"}, "tzdata": {:hex, :tzdata, "0.1.5"}, } diff --git a/test/xmlrpc/tesla/middleware_test.exs b/test/xmlrpc/tesla/middleware_test.exs new file mode 100644 index 0000000..3e8b182 --- /dev/null +++ b/test/xmlrpc/tesla/middleware_test.exs @@ -0,0 +1,233 @@ +defmodule XMLRPC.Tesla.MiddlewareTest do + use ExUnit.Case + + describe "basic" do + defmodule Client do + use Tesla + + plug(XMLRPC.Tesla.Middleware) + + adapter(fn env -> + {status, headers, body} = + case env.url do + "/decode" -> + {200, [{"content-type", "application/xml"}], ~s( + + + + + 30 + + +)} + + "/encode" -> + {200, [{"content-type", "application/xml"}], + env.body |> String.replace("foo", "baz")} + + "/empty" -> + {200, [{"content-type", "application/xml"}], nil} + + "/empty-string" -> + {200, [{"content-type", "application/xml"}], ""} + + "/invalid-content-type" -> + {200, [{"content-type", "text/plain"}], "hello"} + + "/invalid-xml-format" -> + {200, [{"content-type", "application/xml"}], "{\"foo\": bar}"} + + "/invalid-xml-encoding" -> + {200, [{"content-type", "application/xml"}], + <<123, 34, 102, 111, 111, 34, 58, 32, 34, 98, 225, 114, 34, 125>>} + + "/raw" -> + {200, [], env.body} + end + + {:ok, %{env | status: status, headers: headers, body: body}} + end) + end + + test "decode XMLRPC body" do + assert {:ok, env} = Client.get("/decode") + assert env.body == %XMLRPC.MethodResponse{param: 30} + end + + test "do not decode empty body" do + assert {:ok, env} = Client.get("/empty") + assert env.body == nil + end + + test "do not decode empty string body" do + assert {:ok, env} = Client.get("/empty-string") + assert env.body == "" + end + + test "decode only if Content-Type is application/xml or test/json" do + assert {:ok, env} = Client.get("/invalid-content-type") + assert env.body == "hello" + end + + test "encode body as XMLRPC" do + sum = ~s( + + + sample.sum + + + foo + + + + ) + + assert {:ok, env} = Client.post("/encode", sum) + assert env.body == %XMLRPC.MethodCall{method_name: "sample.sum", params: ["baz"]} + end + + test "do not encode nil body" do + assert {:ok, env} = Client.post("/raw", nil) + assert env.body == nil + end + + test "do not encode binary body" do + assert {:ok, env} = Client.post("/raw", "raw-string") + assert env.body == "raw-string" + end + + test "return error on encoding error" do + assert {:error, + {XMLRPC.Tesla.Middleware, :encode, + {_, <<"unable to encode value: ", _rest::binary>>}}} = + Client.post("/encode", %XMLRPC.MethodCall{params: [self()]}) + end + + test "return error when decoding invalid xml format" do + assert {:error, {XMLRPC.Tesla.Middleware, :decode, _}} = Client.get("/invalid-xml-format") + end + + test "raise error when decoding non-utf8 xml" do + assert {:error, {XMLRPC.Tesla.Middleware, :decode, _}} = Client.get("/invalid-xml-encoding") + end + end + + describe "custom decode function" do + defmodule CustomDecodeFunctionClient do + use Tesla + + plug(XMLRPC.Tesla.Middleware, + decode: fn body -> + result = XMLRPC.decode!(body) + + case result.param do + "OK" -> {:ok, {:ok, result}} + "ERROR" -> {:ok, {:error, result}} + end + end + ) + + adapter(fn env -> + body = ~s( + + + + RESULT + + +) + + {status, headers, body} = + case env.url do + "/decode/ok" -> + {200, [{"content-type", "application/xml"}], body |> String.replace("RESULT", "OK")} + + "/decode/error" -> + {200, [{"content-type", "application/xml"}], + body |> String.replace("RESULT", "ERROR")} + end + + {:ok, %{env | status: status, headers: headers, body: body}} + end) + end + + test "decodes as ok if response contains ok" do + assert {:ok, %{body: {:ok, %XMLRPC.MethodResponse{}}}} = + CustomDecodeFunctionClient.get("/decode/ok") + end + + test "decodes as error if response contains error" do + assert {:ok, %{body: {:error, %XMLRPC.MethodResponse{}}}} = + CustomDecodeFunctionClient.get("/decode/error") + end + end + + describe "custom content type" do + defmodule CustomContentTypeClient do + use Tesla + + plug(XMLRPC.Tesla.Middleware, decode_content_types: ["application/x-custom-xml"]) + + adapter(fn env -> + {status, headers, body} = + case env.url do + "/decode" -> + body = ~s( + + + + 30 + + +) + {200, [{"content-type", "application/x-custom-xml"}], body} + end + + {:ok, %{env | status: status, headers: headers, body: body}} + end) + end + + test "decode if Content-Type specified in :decode_content_types" do + assert {:ok, env} = CustomContentTypeClient.get("/decode") + assert env.body == %XMLRPC.MethodResponse{param: 30} + end + + test "set custom request Content-Type header specified in :encode_content_type" do + assert {:ok, env} = + XMLRPC.Tesla.Middleware.call( + %Tesla.Env{body: %XMLRPC.MethodCall{method_name: "some.api", params: [1]}}, + [], + encode_content_type: "application/x-other-custom-xml" + ) + + assert Tesla.get_header(env, "content-type") == "application/x-other-custom-xml" + end + end + + describe "Encode / Decode" do + defmodule EncodeDecodeXMLRPCClient do + use Tesla + + plug(XMLRPC.Tesla.Middleware.Encode) + plug(XMLRPC.Tesla.Middleware.Decode) + + adapter(fn env -> + {status, headers, body} = + case env.url do + "/foo2baz" -> + {200, [{"content-type", "application/xml"}], + env.body |> String.replace("foo", "baz")} + end + + {:ok, %{env | status: status, headers: headers, body: body}} + end) + end + + test "EncodeJson / DecodeJson work without options" do + assert {:ok, env} = + EncodeDecodeXMLRPCClient.post("/foo2baz", %XMLRPC.MethodCall{params: ["foo"]}) + + assert env.body == %XMLRPC.MethodCall{method_name: [], params: ["baz"]} + end + end +end From a1e0944fba9f18bfa55e8f13e1f073fd7b1f7752 Mon Sep 17 00:00:00 2001 From: Michal Darda Date: Mon, 23 Mar 2020 22:43:18 +0000 Subject: [PATCH 2/4] Fix typo --- test/xmlrpc/tesla/middleware_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/xmlrpc/tesla/middleware_test.exs b/test/xmlrpc/tesla/middleware_test.exs index 3e8b182..41dbd51 100644 --- a/test/xmlrpc/tesla/middleware_test.exs +++ b/test/xmlrpc/tesla/middleware_test.exs @@ -223,7 +223,7 @@ defmodule XMLRPC.Tesla.MiddlewareTest do end) end - test "EncodeJson / DecodeJson work without options" do + test "Encode / Decode middlewares work without options" do assert {:ok, env} = EncodeDecodeXMLRPCClient.post("/foo2baz", %XMLRPC.MethodCall{params: ["foo"]}) From 6dd0dd4cf72b4f8b52125121117c43eacd35e864 Mon Sep 17 00:00:00 2001 From: Michal Darda Date: Mon, 23 Mar 2020 22:44:50 +0000 Subject: [PATCH 3/4] Mix format entire project --- lib/xml_rpc.ex | 34 +-- lib/xml_rpc/base64.ex | 13 +- lib/xml_rpc/date_time.ex | 34 ++- lib/xml_rpc/decoder.ex | 139 +++++---- lib/xml_rpc/encoder.ex | 115 ++++--- test/xmlrpc_test.exs | 632 ++++++++++++++++++++------------------- 6 files changed, 500 insertions(+), 467 deletions(-) diff --git a/lib/xml_rpc.ex b/lib/xml_rpc.ex index 77f4383..de99960 100644 --- a/lib/xml_rpc.ex +++ b/lib/xml_rpc.ex @@ -4,7 +4,6 @@ defmodule XMLRPC do alias XMLRPC.Decoder alias XMLRPC.Encoder - @moduledoc ~S""" Encode and decode elixir terms to [XML-RPC](http://wikipedia.org/wiki/XML-RPC) parameters. All XML-RPC parameter types are supported, including arrays, structs and Nil (optional). @@ -90,7 +89,7 @@ defmodule XMLRPC do @moduledoc """ struct defining an xml-rpc 'fault' response """ - @type t :: %__MODULE__{fault_code: Integer, fault_string: String.t} + @type t :: %__MODULE__{fault_code: Integer, fault_string: String.t()} defstruct fault_code: 0, fault_string: "" end @@ -99,7 +98,7 @@ defmodule XMLRPC do @moduledoc """ struct defining an xml-rpc call (note array of params) """ - @type t :: %__MODULE__{method_name: String.t, params: [ XMLRPC.t ]} + @type t :: %__MODULE__{method_name: String.t(), params: [XMLRPC.t()]} defstruct method_name: "", params: nil end @@ -108,21 +107,19 @@ defmodule XMLRPC do @moduledoc """ struct defining an xml-rpc response (note single param) """ - @type t :: %__MODULE__{param: XMLRPC.t} + @type t :: %__MODULE__{param: XMLRPC.t()} defstruct param: nil end - - @type t :: nil | number | boolean | String.t | map() | [nil | number | boolean | String.t] - + @type t :: nil | number | boolean | String.t() | map() | [nil | number | boolean | String.t()] @doc """ Encode an XMLRPC call or response elixir structure into XML as iodata Raises an exception on error. """ - @spec encode_to_iodata!(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:error, {any, String.t}} + @spec encode_to_iodata!(XMLRPC.t(), Keyword.t()) :: {:ok, iodata} | {:error, {any, String.t()}} def encode_to_iodata!(value, options \\ []) do encode!(value, [iodata: true] ++ options) end @@ -130,7 +127,7 @@ defmodule XMLRPC do @doc """ Encode an XMLRPC call or response elixir structure into XML as iodata """ - @spec encode_to_iodata(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:error, {any, String.t}} + @spec encode_to_iodata(XMLRPC.t(), Keyword.t()) :: {:ok, iodata} | {:error, {any, String.t()}} def encode_to_iodata(value, options \\ []) do encode(value, [iodata: true] ++ options) end @@ -143,12 +140,12 @@ defmodule XMLRPC do iex> %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} |> XMLRPC.encode! "test.sumprod23" """ - @spec encode!(XMLRPC.t, Keyword.t) :: iodata | no_return + @spec encode!(XMLRPC.t(), Keyword.t()) :: iodata | no_return def encode!(value, options \\ []) do iodata = Encoder.encode!(value, options) unless options[:iodata] do - iodata |> IO.iodata_to_binary + iodata |> IO.iodata_to_binary() else iodata end @@ -160,26 +157,28 @@ defmodule XMLRPC do iex> %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} |> XMLRPC.encode {:ok, "test.sumprod23"} """ - @spec encode(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:ok, String.t} | {:error, {any, String.t}} + @spec encode(XMLRPC.t(), Keyword.t()) :: + {:ok, iodata} | {:ok, String.t()} | {:error, {any, String.t()}} def encode(value, options \\ []) do {:ok, encode!(value, options)} - rescue exception in [EncodeError] -> {:error, {exception.value, exception.message}} end - @doc ~S""" Decode XMLRPC call or response XML into an Elixir structure iex> XMLRPC.decode("56") {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} """ - @spec decode(iodata, Keyword.t) :: {:ok, Fault.t} | {:ok, MethodCall.t} | {:ok, MethodResponse.t} | {:error, String.t} + @spec decode(iodata, Keyword.t()) :: + {:ok, Fault.t()} + | {:ok, MethodCall.t()} + | {:ok, MethodResponse.t()} + | {:error, String.t()} def decode(value, options \\ []) do {:ok, decode!(value, options)} - rescue exception in [DecodeError] -> {:error, exception.message} @@ -193,7 +192,8 @@ defmodule XMLRPC do iex> XMLRPC.decode!("56") %XMLRPC.MethodResponse{param: [5, 6]} """ - @spec decode!(iodata, Keyword.t) :: Fault.t | MethodCall.t | MethodResponse.t | no_return + @spec decode!(iodata, Keyword.t()) :: + Fault.t() | MethodCall.t() | MethodResponse.t() | no_return def decode!(value, options \\ []) do Decoder.decode!(value, options) end diff --git a/lib/xml_rpc/base64.ex b/lib/xml_rpc/base64.ex index 5906eab..b3a5153 100644 --- a/lib/xml_rpc/base64.ex +++ b/lib/xml_rpc/base64.ex @@ -4,7 +4,7 @@ defmodule XMLRPC.Base64 do Note: See the `Base` module for other conversions in Elixir stdlib """ - @type t :: %__MODULE__{raw: String.t} + @type t :: %__MODULE__{raw: String.t()} defstruct raw: "" @doc """ @@ -24,12 +24,15 @@ defmodule XMLRPC.Base64 do # The <1.2.0 version of elixir won't correctly parse it. # We manually remove whitespace on older versions of elixir case encoded do - [] -> {:ok, encoded} + [] -> + {:ok, encoded} + _ -> - if Version.compare(System.version, "1.2.3") == :lt do + if Version.compare(System.version(), "1.2.3") == :lt do encoded - |> String.replace(~r/\s/, "") # remove any whitespace - |> Base.decode64 + # remove any whitespace + |> String.replace(~r/\s/, "") + |> Base.decode64() else Base.decode64(encoded, ignore: :whitespace) end diff --git a/lib/xml_rpc/date_time.ex b/lib/xml_rpc/date_time.ex index 6d997c8..57f171f 100644 --- a/lib/xml_rpc/date_time.ex +++ b/lib/xml_rpc/date_time.ex @@ -8,7 +8,7 @@ defmodule XMLRPC.DateTime do (and perhaps encoder) to speak to non standard end-points... """ - @type t :: %__MODULE__{raw: String.t} + @type t :: %__MODULE__{raw: String.t()} defstruct raw: "" @doc """ @@ -17,10 +17,14 @@ defmodule XMLRPC.DateTime do iex> XMLRPC.DateTime.new({{2015,6,9},{9,7,2}}) %XMLRPC.DateTime{raw: "20150609T09:07:02"} """ - def new({{year, month, day},{hour, min, sec}}) do - date = :io_lib.format("~4.10.0B~2.10.0B~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B", - [year, month, day, hour, min, sec]) - |> IO.iodata_to_binary + def new({{year, month, day}, {hour, min, sec}}) do + date = + :io_lib.format( + "~4.10.0B~2.10.0B~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B", + [year, month, day, hour, min, sec] + ) + |> IO.iodata_to_binary() + %__MODULE__{raw: date} end @@ -37,14 +41,20 @@ defmodule XMLRPC.DateTime do {:ok, {{2015, 6, 9}, {9, 7, 2}}} """ def to_erlang_date(%__MODULE__{raw: date}) do - case Regex.run(~r/(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):(\d{2}):(\d{2})/, date, capture: :all_but_first) do - nil -> {:error, "Unable to parse date"} - date -> [year, mon, day, hour, min, sec] = - date - |> Enum.map(&to_int/1) - {:ok, {{year, mon, day}, {hour, min, sec}}} + case Regex.run(~r/(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):(\d{2}):(\d{2})/, date, + capture: :all_but_first + ) do + nil -> + {:error, "Unable to parse date"} + + date -> + [year, mon, day, hour, min, sec] = + date + |> Enum.map(&to_int/1) + + {:ok, {{year, mon, day}, {hour, min, sec}}} end end - defp to_int(str), do: str |> Integer.parse |> elem(0) + defp to_int(str), do: str |> Integer.parse() |> elem(0) end diff --git a/lib/xml_rpc/decoder.ex b/lib/xml_rpc/decoder.ex index fb0f2a8..1879af6 100644 --- a/lib/xml_rpc/decoder.ex +++ b/lib/xml_rpc/decoder.ex @@ -3,7 +3,6 @@ defmodule XMLRPC.DecodeError do end defmodule XMLRPC.Decoder do - alias XMLRPC.DecodeError alias XMLRPC.Fault alias XMLRPC.MethodCall @@ -40,15 +39,17 @@ defmodule XMLRPC.Decoder do case :erlsom.scan(xml, model, [{:output_encoding, :utf8}]) do {:error, [{:exception, {_error_type, {error}}}, _stack, _received]} when is_list(error) -> - raise DecodeError, message: List.to_string(error) + raise DecodeError, message: List.to_string(error) + {:error, [{:exception, {_error_type, error}}, _stack, _received]} -> - raise DecodeError, message: error + raise DecodeError, message: error + {:error, message} when is_list(message) -> - raise DecodeError, message: List.to_string(message) + raise DecodeError, message: List.to_string(message) + {:ok, struct, _rest} -> - parse(struct, options) + parse(struct, options) end - end # ########################################################################## @@ -56,100 +57,97 @@ defmodule XMLRPC.Decoder do # Pickup the main type of the thing being parsed and setup appropriate result objects # Parse a method 'Call' - defp parse( {:methodCall, [], method_name, - {:"methodCall/params", [], params }}, - options ) - when is_list(params) - do - %MethodCall{ method_name: method_name, params: parse_params(params, options) } + defp parse( + {:methodCall, [], method_name, {:"methodCall/params", [], params}}, + options + ) + when is_list(params) do + %MethodCall{method_name: method_name, params: parse_params(params, options)} end # Parse a method 'Call' with no (:undefined) params - defp parse( {:methodCall, [], method_name, - {:"methodCall/params", [], :undefined }}, - options ) - do - %MethodCall{ method_name: method_name, params: parse_params([], options) } + defp parse( + {:methodCall, [], method_name, {:"methodCall/params", [], :undefined}}, + options + ) do + %MethodCall{method_name: method_name, params: parse_params([], options)} end # Parse a method 'Call' with completely missing params array - defp parse( {:methodCall, [], method_name, - :undefined}, - options ) - do - %MethodCall{ method_name: method_name, params: parse_params([], options) } + defp parse( + {:methodCall, [], method_name, :undefined}, + options + ) do + %MethodCall{method_name: method_name, params: parse_params([], options)} end # Parse a 'fault' Response - defp parse( {:methodResponse, [], - {:"methodResponse/fault", [], - {:"methodResponse/fault/value", [], - {:"methodResponse/fault/value/struct", [], fault_struct} }}}, - options ) - when is_list(fault_struct) - do + defp parse( + {:methodResponse, [], + {:"methodResponse/fault", [], + {:"methodResponse/fault/value", [], + {:"methodResponse/fault/value/struct", [], fault_struct}}}}, + options + ) + when is_list(fault_struct) do fault = parse_struct(fault_struct, options) fault_code = Map.get(fault, "faultCode") fault_string = Map.get(fault, "faultString") - %Fault{ fault_code: fault_code, fault_string: fault_string } + %Fault{fault_code: fault_code, fault_string: fault_string} end # Parse any other 'Response' - defp parse( {:methodResponse, [], - {:"methodResponse/params", [], param}}, - options ) - when is_tuple(param) - do - %MethodResponse{ param: parse_param(param, options) } + defp parse( + {:methodResponse, [], {:"methodResponse/params", [], param}}, + options + ) + when is_tuple(param) do + %MethodResponse{param: parse_param(param, options)} end # ########################################################################## # Parse an 'array' atom - defp parse_value( {:ValueType, [], [{:ArrayType, [], {:"ArrayType/data", [], array}}]}, options ) do + defp parse_value({:ValueType, [], [{:ArrayType, [], {:"ArrayType/data", [], array}}]}, options) do parse_array(array, options) end # Parse a 'struct' atom - defp parse_value( {:ValueType, [], [{:StructType, [], struct}]}, options) - when is_list(struct) - do + defp parse_value({:ValueType, [], [{:StructType, [], struct}]}, options) + when is_list(struct) do parse_struct(struct, options) end - defp parse_value( {:ValueType, [], [{:StructType, [], _struct}]}, _options) do + defp parse_value({:ValueType, [], [{:StructType, [], _struct}]}, _options) do %{} end # Parse an 'integer' atom - defp parse_value( {:ValueType, [], [{:"ValueType-int", [], int}]}, _options) - when is_integer(int) - do - int + defp parse_value({:ValueType, [], [{:"ValueType-int", [], int}]}, _options) + when is_integer(int) do + int end # Parse an 'i4' atom (32 bit integer) - defp parse_value( {:ValueType, [], [{:"ValueType-i4", [], int}]}, _options) - when is_integer(int) - do - int + defp parse_value({:ValueType, [], [{:"ValueType-i4", [], int}]}, _options) + when is_integer(int) do + int end # Parse an 'i8' atom (64 bit integer) - defp parse_value( {:ValueType, [], [{:"ValueType-i8", [], int}]}, _options) - when is_integer(int) - do - int + defp parse_value({:ValueType, [], [{:"ValueType-i8", [], int}]}, _options) + when is_integer(int) do + int end # Parse a 'float' atom - defp parse_value( {:ValueType, [], [{:"ValueType-double", [], float}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-double", [], float}]}, _options) do Float.parse(float) |> elem(0) end # Parse a 'boolean' atom - defp parse_value( {:ValueType, [], [{:"ValueType-boolean", [], boolean}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-boolean", [], boolean}]}, _options) do case boolean do "0" -> false "1" -> true @@ -157,38 +155,38 @@ defmodule XMLRPC.Decoder do end # Parse a 'datetime' atom (needs decoding from bolloxed iso8601 alike format...) - defp parse_value( {:ValueType, [], [{:"ValueType-dateTime.iso8601", [], datetime}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-dateTime.iso8601", [], datetime}]}, _options) do %XMLRPC.DateTime{raw: datetime} end # Parse a 'base64' atom - defp parse_value( {:ValueType, [], [{:"ValueType-base64", [], string}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-base64", [], string}]}, _options) do %XMLRPC.Base64{raw: string} end # Parse an empty 'string' atom - defp parse_value( {:ValueType, [], [{:"ValueType-string", [], []}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-string", [], []}]}, _options) do "" end # Parse a 'string' atom - defp parse_value( {:ValueType, [], [{:"ValueType-string", [], string}]}, _options) do + defp parse_value({:ValueType, [], [{:"ValueType-string", [], string}]}, _options) do string end # A string value can optionally drop the type specifier. The node is assumed to be a string value - defp parse_value( {:ValueType, [], [string] }, _options) when is_binary(string) do + defp parse_value({:ValueType, [], [string]}, _options) when is_binary(string) do string end # An empty string that drops the type specifier will parse as :undefined instead of an empty binary. - defp parse_value( {:ValueType, [], :undefined }, _options) do + defp parse_value({:ValueType, [], :undefined}, _options) do "" end # Parse a 'nil' atom # Note: this is an xml-rpc extension - defp parse_value( {:ValueType, [], [NilType: []]}, options) do + defp parse_value({:ValueType, [], [NilType: []]}, options) do if options[:exclude_nil] do raise XMLRPC.DecodeError, message: "unable to decode " else @@ -203,11 +201,13 @@ defmodule XMLRPC.Decoder do # Note: values can be 'structs'/'arrays' as well as other atom types defp parse_struct(doc, options) when is_list(doc) do doc - |> Enum.reduce(Map.new, - fn(member, acc) -> - parse_member(member, options) - |> Enum.into(acc) - end) + |> Enum.reduce( + Map.new(), + fn member, acc -> + parse_member(member, options) + |> Enum.into(acc) + end + ) end # Parse the 'array' @@ -230,13 +230,12 @@ defmodule XMLRPC.Decoder do end # Parse a single Parameter - defp parse_param( {:ParamType, [], value }, options ), do: parse_value(value, options) + defp parse_param({:ParamType, [], value}, options), do: parse_value(value, options) # ########################################################################## # Parse one member of a Struct - defp parse_member( {:MemberType, [], name, value }, options ) do + defp parse_member({:MemberType, [], name, value}, options) do [{name, parse_value(value, options)}] end - end diff --git a/lib/xml_rpc/encoder.ex b/lib/xml_rpc/encoder.ex index 46b075f..810e655 100644 --- a/lib/xml_rpc/encoder.ex +++ b/lib/xml_rpc/encoder.ex @@ -2,7 +2,6 @@ defmodule XMLRPC.EncodeError do defexception value: nil, message: nil end - defmodule XMLRPC.Encode do @moduledoc """ Utility functions helpful for encoding XML @@ -36,7 +35,6 @@ defmodule XMLRPC.Encode do end end - defmodule XMLRPC.Encoder do @moduledoc """ This module does the work of encoding an XML-RPC call or response. @@ -46,37 +44,51 @@ defmodule XMLRPC.Encoder do def encode!(%XMLRPC.MethodCall{method_name: method_name, params: params}, options) do [""] ++ - tag("methodCall", - tag("methodName", - method_name) ++ - tag("params", - encode_params(params, options))) - end - - def encode!(%XMLRPC.MethodResponse{ param: param }, options) do + tag( + "methodCall", + tag( + "methodName", + method_name + ) ++ + tag( + "params", + encode_params(params, options) + ) + ) + end + + def encode!(%XMLRPC.MethodResponse{param: param}, options) do [""] ++ - tag("methodResponse", - tag("params", - encode_param(param, options))) + tag( + "methodResponse", + tag( + "params", + encode_param(param, options) + ) + ) end - def encode!(%XMLRPC.Fault{ fault_code: fault_code, fault_string: fault_string }, options) do + def encode!(%XMLRPC.Fault{fault_code: fault_code, fault_string: fault_string}, options) do fault = %{faultCode: fault_code, faultString: fault_string} [""] ++ - tag("methodResponse", - tag("fault", - encode_value(fault, options))) + tag( + "methodResponse", + tag( + "fault", + encode_value(fault, options) + ) + ) end # ########################################################################## defp encode_params(params, options) do - Enum.map params, fn p -> encode_param(p, options) end + Enum.map(params, fn p -> encode_param(p, options) end) end defp encode_param(param, options) do - tag "param", encode_value(param, options) + tag("param", encode_value(param, options)) end # ########################################################################## @@ -84,11 +96,9 @@ defmodule XMLRPC.Encoder do def encode_value(value, options) do tag("value", XMLRPC.ValueEncoder.encode(value, options)) end - end - - # ########################################################################## +# ########################################################################## defprotocol XMLRPC.ValueEncoder do @fallback_to_any true @@ -96,7 +106,6 @@ defprotocol XMLRPC.ValueEncoder do def encode(value, options) end - defimpl XMLRPC.ValueEncoder, for: Atom do import XMLRPC.Encode, only: [tag: 2, escape_attr: 1] @@ -109,33 +118,36 @@ defimpl XMLRPC.ValueEncoder, for: Atom do end end - def encode(true, _options), do: tag("boolean", "1") + def encode(true, _options), do: tag("boolean", "1") def encode(false, _options), do: tag("boolean", "0") - def encode(atom, _options), do: tag("string", - atom - |> Atom.to_string - |> escape_attr ) + def encode(atom, _options), + do: + tag( + "string", + atom + |> Atom.to_string() + |> escape_attr + ) end - defimpl XMLRPC.ValueEncoder, for: BitString do import XMLRPC.Encode, only: [tag: 2, escape_attr: 1] def encode(string, _options) do - tag("string", - escape_attr(string)) + tag( + "string", + escape_attr(string) + ) end end - defimpl XMLRPC.ValueEncoder, for: Integer do import XMLRPC.Encode, only: [tag: 2] def encode(int, _options), do: tag("int", Integer.to_string(int)) end - defimpl XMLRPC.ValueEncoder, for: Float do import XMLRPC.Encode, only: [tag: 2] @@ -156,7 +168,6 @@ defimpl XMLRPC.ValueEncoder, for: Decimal do end end - defimpl XMLRPC.ValueEncoder, for: XMLRPC.DateTime do import XMLRPC.Encode, only: [tag: 2] @@ -165,23 +176,25 @@ defimpl XMLRPC.ValueEncoder, for: XMLRPC.DateTime do end end - defimpl XMLRPC.ValueEncoder, for: XMLRPC.Base64 do import XMLRPC.Encode, only: [tag: 2] def encode(%XMLRPC.Base64{raw: base64}, _options) do - tag("base64", base64) + tag("base64", base64) end end - defimpl XMLRPC.ValueEncoder, for: List do import XMLRPC.Encode, only: [tag: 2] def encode(array, options) do - tag("array", - tag("data", - array |> Enum.map(fn v -> XMLRPC.Encoder.encode_value(v, options) end) ) ) + tag( + "array", + tag( + "data", + array |> Enum.map(fn v -> XMLRPC.Encoder.encode_value(v, options) end) + ) + ) end end @@ -191,8 +204,10 @@ defimpl XMLRPC.ValueEncoder, for: Map do # Parse a general map structure. # Note: This will also match structs, so define those above this definition def encode(struct, options) do - tag("struct", - struct |> Enum.map(fn m -> encode_member(m, options) end)) + tag( + "struct", + struct |> Enum.map(fn m -> encode_member(m, options) end) + ) end # Individual items of a struct. Basically key/value pair @@ -201,9 +216,11 @@ defimpl XMLRPC.ValueEncoder, for: Map do end def encode_member({key, value}, options) when is_bitstring(key) do - tag("member", + tag( + "member", tag("name", escape_attr(key)) ++ - XMLRPC.Encoder.encode_value(value, options) ) + XMLRPC.Encoder.encode_value(value, options) + ) end end @@ -214,11 +231,11 @@ defimpl XMLRPC.ValueEncoder, for: Any do def encode(value, _options) do raise XMLRPC.EncodeError, - value: value, - message: "unable to encode value: #{inspect value}" + value: value, + message: "unable to encode value: #{inspect(value)}" end end - # defp encode_value(_) do - # throw({:error, "Unknown value type"}) - # end +# defp encode_value(_) do +# throw({:error, "Unknown value type"}) +# end diff --git a/test/xmlrpc_test.exs b/test/xmlrpc_test.exs index e7cb8c7..1d91325 100644 --- a/test/xmlrpc_test.exs +++ b/test/xmlrpc_test.exs @@ -5,365 +5,360 @@ defmodule XMLRPC.DecoderTest do doctest XMLRPC.Base64 @rpc_simple_call_1 """ - - - sample.sum - - - 17 - - - - 13 - - - -""" + + + sample.sum + + + 17 + + + + 13 + + + + """ @rpc_simple_call_1_elixir %XMLRPC.MethodCall{method_name: "sample.sum", params: [17, 13]} - # It seems to be valid to either have an empty section or no section at all - @rpc_no_params_1 """ - - - GetAlive - - - -""" - - @rpc_no_params_2 """ - - - GetAlive - -""" + @rpc_no_params_1 """ + + + GetAlive + + + + """ + + @rpc_no_params_2 """ + + + GetAlive + + """ @rpc_no_params_elixir %XMLRPC.MethodCall{method_name: "GetAlive", params: []} - @rpc_simple_response_1 """ - - - - - 30 - - - -""" + + + + + 30 + + + + """ @rpc_simple_response_1_elixir %XMLRPC.MethodResponse{param: 30} - # 2^50 = 1125899906842624 (more than 32 bit) @rpc_bitsize_integer_response_1 """ - - - - - - - - 17 - 1125899906842624 - - - - - - -""" - - @rpc_bitsize_integer_response_1_elixir %XMLRPC.MethodResponse{param: [17, 1125899906842624]} - - + + + + + + + + 17 + 1125899906842624 + + + + + + + """ + + @rpc_bitsize_integer_response_1_elixir %XMLRPC.MethodResponse{ + param: [17, 1_125_899_906_842_624] + } @rpc_fault_1 """ - - - - - - - faultCode - 4 - - - faultString - Too many parameters. - - - - - -""" - - @rpc_fault_1_elixir %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} - - - @rpc_response_all_array """ - - - - - - - - 30 - 1 - 19980717T14:08:55 - -12.53 - Something here - - - - - - - -""" - @rpc_response_all_array_elixir %XMLRPC.MethodResponse{param: - [30, true, - %XMLRPC.DateTime{raw: "19980717T14:08:55"}, - -12.53, "Something here", nil]} - - - @rpc_response_all_struct """ - - - - + + + - bool - 1 - - - datetime - 19980717T14:08:55 - - - double - -12.53 - - - int - 30 + faultCode + 4 - nil - - - - string - Something here + faultString + Too many parameters. - - - -""" - - @rpc_response_all_struct_elixir %XMLRPC.MethodResponse{param: - %{"bool" => true, - "datetime" => %XMLRPC.DateTime{raw: "19980717T14:08:55"}, - "double" => -12.53, "int" => 30, "nil" => nil, - "string" => "Something here"}} + + + """ + @rpc_fault_1_elixir %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} - @rpc_response_nested """ - - - - - - - - - 30 - - - - - - array - + @rpc_response_all_array """ + + + + + + + + 30 + 1 + 19980717T14:08:55 + -12.53 + Something here + + + + + + + + """ + @rpc_response_all_array_elixir %XMLRPC.MethodResponse{ + param: [30, true, %XMLRPC.DateTime{raw: "19980717T14:08:55"}, -12.53, "Something here", nil] + } - - - 30 - - + @rpc_response_all_struct """ + + + + + + + + bool + 1 + + + datetime + 19980717T14:08:55 + + + double + -12.53 + + + int + 30 + + + nil + + + + string + Something here + + + + + + + """ + + @rpc_response_all_struct_elixir %XMLRPC.MethodResponse{ + param: %{ + "bool" => true, + "datetime" => %XMLRPC.DateTime{raw: "19980717T14:08:55"}, + "double" => -12.53, + "int" => 30, + "nil" => nil, + "string" => "Something here" + } + } - - - - - + @rpc_response_nested """ + + + + + - + + + 30 + + + + + + array + + + + + 30 + + + + + + + + - - - - -""" + - @rpc_response_nested_elixir %XMLRPC.MethodResponse{param: - [30, nil, %{"array" => [30]} ]} + + + + + """ + @rpc_response_nested_elixir %XMLRPC.MethodResponse{param: [30, nil, %{"array" => [30]}]} @rpc_response_empty_array """ - - - - - - - - - - - - -""" + + + + + + + + + + + + + """ @rpc_response_empty_array_elixir %XMLRPC.MethodResponse{param: []} - @rpc_response_optional_string_tag """ - - - - - a4sdfff7dad8 - - - -""" + + + + + a4sdfff7dad8 + + + + """ @rpc_response_optional_string_tag_elixir %XMLRPC.MethodResponse{param: "a4sdfff7dad8"} @rpc_response_empty_string_tag """ - - - - - - - - -""" + + + + + + + + + """ @rpc_response_empty_string_tag_elixir %XMLRPC.MethodResponse{param: ""} @rpc_response_optional_empty_string_tag """ - - - - - - - - -""" + + + + + + + + + """ @rpc_response_optional_empty_string_tag_elixir %XMLRPC.MethodResponse{param: ""} @rpc_base64_call_1 """ - - - sample.fun1 - - - 1 - - - YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OQ== - - - -""" + + + sample.fun1 + + + 1 + + + YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OQ== + + + + """ @rpc_base64_call_with_whitespace """ - - - sample.fun1 - - - 1 - - - -YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZm -MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDEx -MjIzMzQ0NTU2Njc3ODg5OQ== - - - - -""" + + + sample.fun1 + + + 1 + + + + YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZm + MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDEx + MjIzMzQ0NTU2Njc3ODg5OQ== + + + + + """ @rpc_base64_value "aabbccddeeffaabbccddeeff0011223344556677889900112233445566778899" - @rpc_base64_call_1_elixir_to_encode %XMLRPC.MethodCall{method_name: "sample.fun1", params: [true, - XMLRPC.Base64.new(@rpc_base64_value)]} + @rpc_base64_call_1_elixir_to_encode %XMLRPC.MethodCall{ + method_name: "sample.fun1", + params: [true, XMLRPC.Base64.new(@rpc_base64_value)] + } # Various malformed tags @rpc_response_invalid_1 """ - - - - - 30 - - - - 30 - - - -""" + + + + + 30 + + + + 30 + + + + """ # Various malformed tags @rpc_response_invalid_2 """ - - - - - 30 - - - -""" + + + + + 30 + + + + """ # Raise an error when trying to encode unsupported param type (function in this case) @rpc_response_invalid_3_elixir %XMLRPC.MethodResponse{param: &Kernel.exit/1} @rpc_response_empty_struct """ - - - - - - - - - - - -""" + + + + + + + + + + + + """ @rpc_response_empty_struct_elixir %XMLRPC.MethodResponse{param: %{}} - # ########################################################################## - test "decode rpc_simple_call_1" do decode = XMLRPC.decode(@rpc_simple_call_1) assert decode == {:ok, @rpc_simple_call_1_elixir} @@ -436,7 +431,7 @@ MjIzMzQ0NTU2Njc3ODg5OQ== test "decode base64 data with whitespace" do {:ok, decode} = XMLRPC.decode(@rpc_base64_call_with_whitespace) - assert {:ok, @rpc_base64_value} == decode.params |> List.last |> XMLRPC.Base64.to_binary + assert {:ok, @rpc_base64_value} == decode.params |> List.last() |> XMLRPC.Base64.to_binary() end test "decode rpc_response_invalid_1" do @@ -456,38 +451,42 @@ MjIzMzQ0NTU2Njc3ODg5OQ== # ########################################################################## - test "encode rpc_simple_call_1" do - encode = XMLRPC.encode!(@rpc_simple_call_1_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_simple_call_1_elixir) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_simple_call_1) end test "encode rpc_simple_response_1" do - encode = XMLRPC.encode!(@rpc_simple_response_1_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_simple_response_1_elixir) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_simple_response_1) end test "encode rpc_fault_1" do - encode = XMLRPC.encode!(@rpc_fault_1_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_fault_1_elixir) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_fault_1) end test "encode rpc_response_all_array" do - encode = XMLRPC.encode!(@rpc_response_all_array_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_response_all_array_elixir) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_response_all_array) end test "encode rpc_response_all_struct" do - encode = XMLRPC.encode!(@rpc_response_all_struct_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_response_all_struct_elixir) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_response_all_struct) end @@ -505,8 +504,9 @@ MjIzMzQ0NTU2Njc3ODg5OQ== end test "encode base64 data" do - encode = XMLRPC.encode!(@rpc_base64_call_1_elixir_to_encode) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_base64_call_1_elixir_to_encode) + |> IO.iodata_to_binary() assert encode == strip_space(@rpc_base64_call_1) end @@ -518,31 +518,35 @@ MjIzMzQ0NTU2Njc3ODg5OQ== end test "encode rpc_response_empty_struct" do - encode = XMLRPC.encode!(@rpc_response_empty_struct_elixir) - |> IO.iodata_to_binary + encode = + XMLRPC.encode!(@rpc_response_empty_struct_elixir) + |> IO.iodata_to_binary() - assert encode == strip_space(@rpc_response_empty_struct) + assert encode == strip_space(@rpc_response_empty_struct) end test "floating point doesn't round arbitrarily" do - assert "127.39" == 127.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary - assert "128.39" == 128.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary + assert "127.39" == + 127.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary() + + assert "128.39" == + 128.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary() end test "Decimal type outputs with expected precision" do - assert "127.39" == Decimal.new("127.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary - assert "128.39" == Decimal.new("128.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary + assert "127.39" == + Decimal.new("127.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary() + + assert "128.39" == + Decimal.new("128.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary() end # ########################################################################## - # Helper functions # defp strip_space(string) do Regex.replace(~r/>\s+<") - |> String.trim + |> String.trim() end - - end From 0cd6411fc31af6472e6430dcd6b467b4d0125042 Mon Sep 17 00:00:00 2001 From: Michal Darda Date: Tue, 24 Mar 2020 10:22:39 +0100 Subject: [PATCH 4/4] Add text/xml content type to default content types --- lib/xml_rpc/tesla/middleware.ex | 2 +- test/xmlrpc/tesla/middleware_test.exs | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/xml_rpc/tesla/middleware.ex b/lib/xml_rpc/tesla/middleware.ex index c57ec86..6566981 100644 --- a/lib/xml_rpc/tesla/middleware.ex +++ b/lib/xml_rpc/tesla/middleware.ex @@ -24,7 +24,7 @@ if Code.ensure_loaded?(Tesla) do @behaviour Tesla.Middleware @default_encode_content_type "application/xml" - @default_content_types ["application/xml"] + @default_content_types ["application/xml", "text/xml"] def default_decodable_status(status), do: status in 200..299 @impl Tesla.Middleware diff --git a/test/xmlrpc/tesla/middleware_test.exs b/test/xmlrpc/tesla/middleware_test.exs index 41dbd51..9a52aa6 100644 --- a/test/xmlrpc/tesla/middleware_test.exs +++ b/test/xmlrpc/tesla/middleware_test.exs @@ -25,6 +25,10 @@ defmodule XMLRPC.Tesla.MiddlewareTest do {200, [{"content-type", "application/xml"}], env.body |> String.replace("foo", "baz")} + "/encode2" -> + {200, [{"content-type", "text/xml"}], + env.body |> String.replace("foo", "baz")} + "/empty" -> {200, [{"content-type", "application/xml"}], nil} @@ -64,12 +68,12 @@ defmodule XMLRPC.Tesla.MiddlewareTest do assert env.body == "" end - test "decode only if Content-Type is application/xml or test/json" do + test "decode only if Content-Type is application/xml or text/xml" do assert {:ok, env} = Client.get("/invalid-content-type") assert env.body == "hello" end - test "encode body as XMLRPC" do + test "encode body as XMLRPC when content type is application/xml" do sum = ~s( @@ -86,6 +90,23 @@ defmodule XMLRPC.Tesla.MiddlewareTest do assert env.body == %XMLRPC.MethodCall{method_name: "sample.sum", params: ["baz"]} end + test "encode body as XMLRPC when content type is text/xml" do + sum = ~s( + + + sample.sum + + + foo + + + + ) + + assert {:ok, env} = Client.post("/encode2", sum) + assert env.body == %XMLRPC.MethodCall{method_name: "sample.sum", params: ["baz"]} + end + test "do not encode nil body" do assert {:ok, env} = Client.post("/raw", nil) assert env.body == nil