defmodule Weebo do @moduledoc ~S""" Weebo is an [XML-RPC](http://wikipedia.org/wiki/XML-RPC) parser/formatter for Elixir, with full data-type support. Weebo can be combined with GenServer, Phoenix, HTTPoison (and others!) to create fully-featured XML-RPC clients & servers. request = Weebo.parse("math.sum123") #=> %Weebo.Request{method: "math.sum", params: [1, 2, 3]} sum = Enum.sum(request.params) response = %Weebo.Response{error: nil, params: [sum]} Weebo.format(response) #=> "6" ## Data Type Mapping All the following data-types are supported, and will be automatically serialized in `format/1` and `parse/1`: | XMLRPC | Elixir | | -------|--------| | `` | Bitstring - `"string"` | | `` | Integer - `8` | | `` | Boolean - `true false `| | `` | Float - `6.3` | | `` | List - `[1, 2, 3]` | | `` | Map - `%{key: "value"}` | | `` | Tuple - `{{2015, 6, 7}, {16, 24, 18}}` | | `` | Nil atom - `nil` | In addition, the following extra data-types are supported only in `parse/1`: | XMLRPC | Elixir | | -------|--------| | `` | Bitstring - `"string"` (will decode the base64 first) | | `` | Integer - `8` | """ @typedoc ~S""" Tuple-representation of an XML element. This is the format Weebo uses internally during parsing/formatting. The first element is an atom that represents the XML node's name, and the second element is a list of child elements that follow the same pattern. ### Example The following bit of XML: Hello Would be represented as: {:value, [{:string, ["Hello"]}]} """ @type xml_tree :: {atom, [String.t | tuple]} alias Weebo.XMLInterface, as: XML alias Weebo.Formattable @doc ~S""" Formats `%Weebo.Response` and `%Weebo.Request` into an XML-RPC string. ## Examples Weebo.format(%Weebo.Response{error: nil, params: [%{success: true}]}) #=> "success1" """ @spec format(Weebo.Request.t | Weebo.Response.t) :: String.t def format(subject) do Formattable.format(subject)|>XML.from_tree|>XML.export end @doc ~S""" Parses an XML-RPC string into `%Weebo.Request` and `%Weebo.Response`. ## Examples Weebo.parse("math.sum14") #=> %Weebo.Request{method: "math.sum", params: [1, 4]} """ @spec parse(String.t) :: Weebo.Request.t | Weebo.Response.t def parse(string) when is_bitstring(string) do parsed = XML.parse(string)|>XML.to_tree case parsed do {:methodCall, [{:methodName, [name]}, {:params, params}]} -> %Weebo.Request{method: name, params: Enum.map(params, &cast/1)} {:methodResponse, [{:params, params}]} -> %Weebo.Response{error: nil, params: Enum.map(params, &cast/1)} {:methodResponse, [{:fault, [error]}]} -> %Weebo.Response{error: cast(error)} end end @doc false @spec cast(String.t) :: xml_tree @spec cast(xml_tree) :: any def cast(string) when is_bitstring(string) do XML.parse(string)|>XML.to_tree|>cast end def cast({:string, [string]}), do: string def cast({:boolean, ["1"]}), do: true def cast({:boolean, ["0"]}), do: false def cast({:int, [int]}), do: String.to_integer(int) def cast({:i4, [int]}), do: String.to_integer(int) def cast({:double, [double]}), do: String.to_float(double) def cast({:base64, [string]}), do: Base.decode64!(string) def cast({:"dateTime.iso8601", [value]}), do: :iso8601.parse(value) def cast({:array, [{:data, items}]}), do: Enum.map(items, &cast/1) def cast({:member, [{:name, [name]}, value]}), do: {String.to_atom(name), cast(value)} def cast({:struct, members}) do {_, casted} = Enum.map_reduce members, %{}, fn(member, acc) -> {name, value} = cast(member) {member, Map.put(acc, name, value)} end casted end def cast({:value, [value]}), do: cast(value) def cast({:param, [value]}), do: cast(value) def cast({:nil, []}), do: nil def cast(val) do {:unknown_type, val} end end