Current section
Files
Jump to
Current section
Files
lib/sweet_xml/sweet_xml.ex
# NOTE: this has been yanked from https://github.com/peek-travel/sweet_xml
# because the maintainers of the library have gone MIA and are not merging PRs
defmodule SweetXpath do
@moduledoc false
defmodule Priv do
@moduledoc false
def self_val(val), do: val
end
defstruct path: ".",
is_value: true,
is_list: false,
is_keyword: false,
is_optional: false,
cast_to: false,
transform_fun: &Priv.self_val/1,
namespaces: []
end
defmodule SweetXml do
@moduledoc false
import Record, only: [defrecord: 2, extract: 2]
@xmerl "xmerl/include/xmerl.hrl"
defrecord :xmlDecl, extract(:xmlDecl, from_lib: @xmerl)
defrecord :xmlAttribute, extract(:xmlAttribute, from_lib: @xmerl)
defrecord :xmlNamespace, extract(:xmlNamespace, from_lib: @xmerl)
defrecord :xmlNsNode, extract(:xmlNsNode, from_lib: @xmerl)
defrecord :xmlElement, extract(:xmlElement, from_lib: @xmerl)
defrecord :xmlText, extract(:xmlText, from_lib: @xmerl)
defrecord :xmlComment, extract(:xmlComment, from_lib: @xmerl)
defrecord :xmlPI, extract(:xmlPI, from_lib: @xmerl)
defrecord :xmlDocument, extract(:xmlDocument, from_lib: @xmerl)
defrecord :xmlObj, extract(:xmlObj, from_lib: @xmerl)
def sigil_x(path, modifiers \\ '') do
%SweetXpath{
path: String.to_charlist(path),
is_value: not (?e in modifiers),
is_list: ?l in modifiers,
is_keyword: ?k in modifiers,
is_optional: ?o in modifiers,
cast_to:
cond do
?s in modifiers -> :string
?S in modifiers -> :soft_string
?i in modifiers -> :integer
?I in modifiers -> :soft_integer
?f in modifiers -> :float
?F in modifiers -> :soft_float
?b in modifiers -> :boolean
:otherwise -> false
end
}
end
def add_namespace(xpath, prefix, uri) do
%SweetXpath{
xpath
| namespaces: [
{to_charlist(prefix), to_charlist(uri)}
| xpath.namespaces
]
}
end
def parse(doc), do: parse(doc, [])
def parse(doc, options) when is_binary(doc) do
doc |> :erlang.binary_to_list() |> parse(options)
end
def parse([c | _] = doc, options) when is_integer(c) do
{parsed_doc, _} = :xmerl_scan.string(doc, options)
parsed_doc
end
def parse(doc_enum, options) do
{parsed_doc, _} = :xmerl_scan.string('', options ++ continuation_opts(doc_enum))
parsed_doc
end
def stream_tags(doc, tags, options \\ []) do
tags = if is_atom(tags), do: [tags], else: tags
{discard_tags, xmerl_options} =
if options[:discard] do
{options[:discard], Keyword.delete(options, :discard)}
else
{[], options}
end
doc
|> stream(fn emit ->
[
hook_fun: fn
entity, xstate when Record.is_record(entity, :xmlElement) ->
name = xmlElement(entity, :name)
if length(tags) == 0 or name in tags do
emit.({name, entity})
end
{entity, xstate}
entity, xstate ->
{entity, xstate}
end,
acc_fun: fn
entity, acc, xstate when Record.is_record(entity, :xmlElement) ->
if xmlElement(entity, :name) in discard_tags do
{acc, xstate}
else
{[entity | acc], xstate}
end
entity, acc, xstate ->
{[entity | acc], xstate}
end
] ++ xmerl_options
end)
end
def stream(doc, options_callback) when is_binary(doc) do
stream([doc], options_callback)
end
def stream([c | _] = doc, options_callback) when is_integer(c) do
stream([IO.iodata_to_binary(doc)], options_callback)
end
def stream(doc, options_callback) do
Stream.resource(
fn ->
{parent, ref} = waiter = {self(), make_ref()}
opts = options_callback.(fn e -> send(parent, {:event, ref, e}) end)
pid = spawn_link(fn -> :xmerl_scan.string('', opts ++ continuation_opts(doc, waiter)) end)
{ref, pid, Process.monitor(pid)}
end,
fn {ref, pid, monref} = acc ->
receive do
{:DOWN, ^monref, _, _, _} ->
## !!! maybe do something when reason !== :normal
{:halt, :parse_ended}
{:event, ^ref, event} ->
{[event], acc}
{:wait, ^ref} ->
send(pid, {:continue, ref})
{[], acc}
end
end,
fn
:parse_ended ->
:ok
{ref, pid, monref} ->
Process.demonitor(monref)
flush_halt(pid, ref)
end
)
end
def xpath(parent, spec) when not is_tuple(parent) do
parent |> parse |> xpath(spec)
end
def xpath(
parent,
%SweetXpath{is_list: true, is_value: true, cast_to: cast, is_optional: is_opt?} = spec
) do
get_current_entities(parent, spec)
|> Enum.map(&(_value(&1) |> to_cast(cast, is_opt?)))
|> spec.transform_fun.()
end
def xpath(parent, %SweetXpath{is_list: true, is_value: false} = spec) do
get_current_entities(parent, spec) |> spec.transform_fun.()
end
def xpath(
parent,
%SweetXpath{is_list: false, is_value: true, cast_to: string_type, is_optional: is_opt?} = spec
)
when string_type in [:string, :soft_string] do
spec = %SweetXpath{spec | is_list: true}
get_current_entities(parent, spec)
|> Enum.map(&(_value(&1) |> to_cast(string_type, is_opt?)))
|> case do
[] -> nil
items -> Enum.join(items)
end
|> spec.transform_fun.()
end
def xpath(
parent,
%SweetXpath{is_list: false, is_value: true, cast_to: cast, is_optional: is_opt?} = spec
) do
get_current_entities(parent, spec)
|> _value
|> to_cast(cast, is_opt?)
|> spec.transform_fun.()
end
def xpath(parent, %SweetXpath{is_list: false, is_value: false} = spec) do
get_current_entities(parent, spec) |> spec.transform_fun.()
end
def xpath(parent, sweet_xpath, subspec) do
if sweet_xpath.is_list do
current_entities = xpath(parent, sweet_xpath)
Enum.map(current_entities, fn entity -> xmap(entity, subspec, sweet_xpath) end)
else
current_entity = xpath(parent, sweet_xpath)
xmap(current_entity, subspec, sweet_xpath)
end
end
def xmap(parent, mapping), do: xmap(parent, mapping, %{is_keyword: false})
def xmap(nil, _, %{is_optional: true}), do: nil
def xmap(parent, [], atom) when is_atom(atom), do: xmap(parent, [], %{is_keyword: atom})
def xmap(_, [], %{is_keyword: false}), do: %{}
def xmap(_, [], %{is_keyword: true}), do: []
def xmap(parent, [{label, spec} | tail], is_keyword) when is_list(spec) do
[sweet_xpath | subspec] = spec
result = xmap(parent, tail, is_keyword)
put_in(result[label], xpath(parent, sweet_xpath, subspec))
end
def xmap(parent, [{label, sweet_xpath} | tail], is_keyword) do
result = xmap(parent, tail, is_keyword)
put_in(result[label], xpath(parent, sweet_xpath))
end
def transform_by(%SweetXpath{} = sweet_xpath, fun) when is_function(fun) do
%{sweet_xpath | transform_fun: fun}
end
defp _value(entity) do
cond do
is_record?(entity, :xmlText) ->
xmlText(entity, :value)
is_record?(entity, :xmlComment) ->
xmlComment(entity, :value)
is_record?(entity, :xmlPI) ->
xmlPI(entity, :value)
is_record?(entity, :xmlAttribute) ->
xmlAttribute(entity, :value)
is_record?(entity, :xmlObj) ->
xmlObj(entity, :value)
true ->
entity
end
end
defp is_record?(data, kind) do
is_tuple(data) and tuple_size(data) > 0 and :erlang.element(1, data) == kind
end
defp continuation_opts(enum, waiter \\ nil) do
[
{
:continuation_fun,
fn xcont, xexc, xstate ->
case :xmerl_scan.cont_state(xstate).({:cont, []}) do
{:halted, _acc} ->
xexc.(xstate)
{:suspended, bin, cont} ->
case waiter do
nil ->
:ok
{parent, ref} ->
# continuation behaviour, pause and wait stream decision
send(parent, {:wait, ref})
receive do
# stream continuation fun has been called: parse to find more elements
{:continue, ^ref} ->
:ok
# stream halted: halt the underlying stream and exit parsing process
{:halt, ^ref} ->
cont.({:halt, []})
exit(:normal)
end
end
xcont.(bin, :xmerl_scan.cont_state(cont, xstate))
{:done, _} ->
xexc.(xstate)
end
end,
&Enumerable.reduce(split_by_whitespace(enum), &1, fn bin, _ -> {:suspend, bin} end)
},
{
:close_fun,
# make sure the XML end halts the binary stream (if more bytes are available after XML)
fn xstate ->
:xmerl_scan.cont_state(xstate).({:halt, []})
xstate
end
}
]
end
defp split_by_whitespace(enum) do
reducer = fn
:last, prev ->
{[:erlang.binary_to_list(prev)], :done}
bin, prev ->
bin = if prev === "", do: bin, else: IO.iodata_to_binary([prev, bin])
case split_last_whitespace(bin) do
:white_bin -> {[], bin}
{head, tail} -> {[:erlang.binary_to_list(head)], tail}
end
end
Stream.concat(enum, [:last]) |> Stream.transform("", reducer)
end
defp split_last_whitespace(bin), do: split_last_whitespace(byte_size(bin) - 1, bin)
defp split_last_whitespace(0, _), do: :white_bin
defp split_last_whitespace(size, bin) do
case bin do
<<_::binary-size(size), h>> <> tail when h == ?\s or h == ?\n or h == ?\r or h == ?\t ->
{head, _} = :erlang.split_binary(bin, size + 1)
{head, tail}
_ ->
split_last_whitespace(size - 1, bin)
end
end
defp flush_halt(pid, ref) do
receive do
{:event, ^ref, _} ->
# flush all emitted elems after :halt
flush_halt(pid, ref)
{:wait, ^ref} ->
# tell the continuation function to halt the underlying stream
send(pid, {:halt, ref})
end
end
defp get_current_entities(parent, %SweetXpath{path: path, is_list: true, namespaces: namespaces}) do
:xmerl_xpath.string(path, parent, namespace: namespaces) |> List.wrap()
end
defp get_current_entities(parent, %SweetXpath{
path: path,
is_list: false,
namespaces: namespaces
}) do
ret = :xmerl_xpath.string(path, parent, namespace: namespaces)
if is_record?(ret, :xmlObj) do
ret
else
List.first(ret)
end
end
defp to_cast(value, false, _is_opt?), do: value
defp to_cast(nil, _, true), do: nil
defp to_cast(value, :string, _is_opt?), do: to_string(value)
defp to_cast(value, :integer, _is_opt?), do: String.to_integer(to_string(value))
defp to_cast(value, :float, _is_opt?) do
{float, _} = Float.parse(to_string(value))
float
end
defp to_cast('true', :boolean, _is_opt?), do: true
defp to_cast('false', :boolean, _is_opt?), do: false
defp to_cast(value, :soft_string, is_opt?) do
if String.Chars.impl_for(value) do
to_string(value)
else
if is_opt?, do: nil, else: ""
end
end
defp to_cast(value, :soft_integer, is_opt?) do
if String.Chars.impl_for(value) do
case Integer.parse(to_string(value)) do
:error -> if is_opt?, do: nil, else: 0
{int, _} -> int
end
else
if is_opt?, do: nil, else: 0
end
end
defp to_cast(value, :soft_float, is_opt?) do
if String.Chars.impl_for(value) do
case Float.parse(to_string(value)) do
:error -> if is_opt?, do: nil, else: 0.0
{float, _} -> float
end
else
if is_opt?, do: nil, else: 0.0
end
end
end