Packages

Effortless XML parsing for Erlang, simple, fast, and neat.

Current section

Files

Jump to
loki_xml src loki_xml.erl
Raw

src/loki_xml.erl

%%%-------------------------------------------------------------------
%%% @doc loki_xml - Main API for distributed XML parser
%%% @end
%%%-------------------------------------------------------------------
-module(loki_xml).
-export([
parse/1, parse/2,
query/2,
to_map/1,
start/0, stop/0
]).
-type xml_element() :: {element, binary(), [{binary(), binary()}], [xml_element() | binary()]}.
-type parse_opts() :: #{
format => term | map,
distributed => boolean(),
nodes => [node()],
chunk_size => pos_integer(),
tag => binary(),
max_retries => non_neg_integer()
}.
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Start the loki_xml application
-spec start() -> ok | {error, term()}.
start() ->
application:ensure_all_started(loki_xml).
%% @doc Stop the loki_xml application
-spec stop() -> ok.
stop() ->
application:stop(loki_xml).
%% @doc Parse XML file with default options
-spec parse(file:filename() | binary()) -> {ok, [xml_element()]} | {error, term()}.
parse(Source) ->
parse(Source, #{}).
%% @doc Parse XML file with options
-spec parse(file:filename() | binary(), parse_opts()) -> {ok, [xml_element()]} | {error, term()}.
parse(Source, Opts) ->
Format = maps:get(format, Opts, term),
Distributed = maps:get(distributed, Opts, false),
Nodes = maps:get(nodes, Opts, [node()]),
ChunkSize = maps:get(chunk_size, Opts, 1048576), % 1MB default
Tag = maps:get(tag, Opts, undefined),
%% Read source
Binary = case Source of
B when is_binary(B) -> B;
Filename when is_list(Filename) ->
case file:read_file(Filename) of
{ok, Data} -> Data;
{error, Reason} -> throw({error, {read_file, Reason}})
end
end,
%% Split into chunks
{ok, Chunks} = case Tag of
undefined ->
loki_xml_splitter:split_fixed(Binary, ChunkSize);
TagName ->
loki_xml_splitter:split_by_tag(Binary, TagName)
end,
%% Parse chunks
ParsedChunks = case Distributed of
true ->
loki_xml_dist:distribute(Nodes, Chunks);
false ->
parse_chunks_local(Chunks, Opts)
end,
%% Merge results
{ok, Merged} = loki_xml_merger:merge(ParsedChunks),
%% Format conversion
Result = case Format of
map -> lists:map(fun to_map/1, Merged);
_ -> Merged
end,
{ok, Result}.
%% @doc Run XPath query on parsed data
-spec query([xml_element()], binary()) -> [binary() | xml_element()].
query(Parsed, XPath) ->
loki_xml_query:query(Parsed, XPath).
%% @doc Convert XML element to map
-spec to_map(xml_element()) -> map().
to_map({element, Tag, Attrs, Children}) ->
ChildMaps = lists:map(fun
(C) when is_binary(C) -> C;
(C) -> to_map(C)
end, Children),
#{
tag => Tag,
attributes => maps:from_list(Attrs),
children => ChildMaps
}.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
parse_chunks_local(Chunks, Opts) ->
MaxRetries = maps:get(max_retries, Opts, 3),
Parent = self(),
%% Spawn workers for each chunk
Workers = lists:map(fun(Chunk) ->
spawn_link(fun() ->
Result = parse_chunk_with_retry(Chunk, MaxRetries),
Parent ! {self(), Result}
end)
end, Chunks),
%% Collect results
collect_results(Workers, []).
parse_chunk_with_retry(Chunk, 0) ->
{error, {max_retries, Chunk}};
parse_chunk_with_retry(Chunk, Retries) ->
case loki_xml_parser:parse(Chunk) of
{ok, Parsed} -> {ok, Parsed};
{error, _} -> parse_chunk_with_retry(Chunk, Retries - 1)
end.
collect_results([], Acc) ->
lists:reverse(Acc);
collect_results([Worker|Rest], Acc) ->
receive
{Worker, Result} ->
collect_results(Rest, [Result|Acc])
after 30000 ->
{error, timeout}
end.