if Code.ensure_loaded?(Saxy) do
defmodule WeChat.ServerMessage.XmlParser do
@moduledoc "XML Parser"
@behaviour Saxy.Handler
@spec parse(xml :: String.t()) :: {:ok, map()}
def parse(xml) do
Saxy.parse_string(xml, __MODULE__, [])
end
@impl true
def handle_event(:start_document, _prolog, stack) do
{:ok, stack}
end
def handle_event(:start_element, {element, _attributes}, stack) do
{:ok, [{element, []} | stack]}
end
def handle_event(:characters, chars, [{element, content} | stack] = old) do
case String.trim(chars) do
"" -> {:ok, old}
chars -> {:ok, [{element, [chars | content]} | stack]}
end
end
def handle_event(:end_element, tag_name, stack) do
[{^tag_name, content} | stack] = stack
current = {tag_name, Enum.reverse(content)}
case stack do
[] ->
{:ok, [current]}
[{parent_tag_name, parent_content} | rest] ->
parent = {parent_tag_name, [current | parent_content]}
{:ok, [parent | rest]}
end
end
def handle_event(:end_document, _data, stack) do
state = stack |> stack_to_map() |> Map.get("xml")
{:ok, state}
end
defp stack_to_map(stack) do
Map.new(stack, fn
{name, []} ->
{name, ""}
{name, [content]} when is_binary(content) ->
{name, content}
{name, content} ->
with [{"item", _}] <- Enum.uniq_by(content, &elem(&1, 0)) do
content = Enum.map(content, &(&1 |> elem(1) |> stack_to_map()))
{name, content}
else
_ ->
{name, stack_to_map(content)}
end
end)
end
end
end