Skip to main content

src/aws@internal@codec@xml_decode.erl

-module(aws@internal@codec@xml_decode).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/codec/xml_decode.gleam").
-export([parse/1, find_child/2, find_children/2, text_content/1, attr/2, string_text/1, bool_text/1, int_text/1, float_text/1, smithy_float_text/1, timestamp_text/1, timestamp_text_precise/1, optional_child/3, required_child/3, optional_list/4, required_list/4, optional_flat_list/3, required_flat_list/3, inner_list/3]).
-export_type([element/0, node_/0, raw_node/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " XML decoder for restXml / awsQuery / ec2Query responses.\n"
    "\n"
    " The Erlang side (`aws_ffi:xml_parse/1`) does the heavy lifting via\n"
    " xmerl from the OTP standard library. It returns a `Element` tree\n"
    " shaped as nested tuples; on the Gleam side we expose that tree and\n"
    " a handful of accessor helpers that the generated decoders call\n"
    " (`find_child`, `find_children`, `child_text`, ...).\n"
    "\n"
    " Whitespace-only text nodes between elements are stripped on the\n"
    " Erlang side so the generated code can address members by element\n"
    " name without thinking about layout. Repeated child elements (used\n"
    " for `@xmlFlattened` lists) are surfaced by `find_children`.\n"
).

-type element() :: {element,
        binary(),
        list({binary(), binary()}),
        list(node_())}.

-type node_() :: {element_node, element()} | {text, binary()}.

-type raw_node() :: any().

-file("src/aws/internal/codec/xml_decode.gleam", 60).
-spec tag_of(raw_node()) -> binary().
tag_of(T) ->
    erlang:atom_to_binary(erlang:element(1, T)).

-file("src/aws/internal/codec/xml_decode.gleam", 73).
-spec node_from_tuple(raw_node()) -> node_().
node_from_tuple(T) ->
    case tag_of(T) of
        <<"element"/utf8>> ->
            Name = gleam@function:identity(erlang:element(2, T)),
            Attrs_raw = gleam@function:identity(erlang:element(3, T)),
            Children_raw = gleam@function:identity(erlang:element(4, T)),
            Attrs = gleam@list:map(
                Attrs_raw,
                fun(P) ->
                    {gleam@function:identity(erlang:element(1, P)),
                        gleam@function:identity(erlang:element(2, P))}
                end
            ),
            Children = gleam@list:map(Children_raw, fun node_from_tuple/1),
            {element_node, {element, Name, Attrs, Children}};

        <<"text"/utf8>> ->
            {text, gleam@function:identity(erlang:element(2, T))};

        _ ->
            {text, <<""/utf8>>}
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 39).
?DOC(
    " Parse an XML document into an `Element`. Returns `Error(\"...\")` on\n"
    " malformed input — generated decoders propagate this up as a\n"
    " `DecodeError`.\n"
).
-spec parse(binary()) -> {ok, element()} | {error, binary()}.
parse(Body) ->
    case aws_ffi:xml_parse(Body) of
        {ok, T} ->
            case node_from_tuple(T) of
                {element_node, E} ->
                    {ok, E};

                _ ->
                    {error, <<"xml: root is not an element"/utf8>>}
            end;

        {error, _} ->
            {error, <<"xml: parse failed"/utf8>>}
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 99).
-spec do_find_child(list(node_()), binary()) -> gleam@option:option(element()).
do_find_child(Nodes, Name) ->
    case Nodes of
        [] ->
            none;

        [{element_node, E} | Rest] ->
            case erlang:element(2, E) =:= Name of
                true ->
                    {some, E};

                false ->
                    do_find_child(Rest, Name)
            end;

        [_ | Rest@1] ->
            do_find_child(Rest@1, Name)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 95).
?DOC(" Find the first child element with the given local name.\n").
-spec find_child(element(), binary()) -> gleam@option:option(element()).
find_child(Parent, Name) ->
    do_find_child(erlang:element(4, Parent), Name).

-file("src/aws/internal/codec/xml_decode.gleam", 115).
?DOC(
    " Find all child elements with the given local name. Used for both\n"
    " `@xmlFlattened` lists (which appear as repeated siblings of the\n"
    " parent) and for normal wrapped lists (after stepping into the\n"
    " wrapper element).\n"
).
-spec find_children(element(), binary()) -> list(element()).
find_children(Parent, Name) ->
    gleam@list:filter_map(erlang:element(4, Parent), fun(N) -> case N of
                {element_node, E} ->
                    case erlang:element(2, E) =:= Name of
                        true ->
                            {ok, E};

                        false ->
                            {error, nil}
                    end;

                _ ->
                    {error, nil}
            end end).

-file("src/aws/internal/codec/xml_decode.gleam", 130).
?DOC(
    " Concatenate all direct text-node children. Used for primitive\n"
    " element values like `<Name>foo</Name>`.\n"
).
-spec text_content(element()) -> binary().
text_content(E) ->
    gleam@list:fold(erlang:element(4, E), <<""/utf8>>, fun(Acc, N) -> case N of
                {text, V} ->
                    <<Acc/binary, V/binary>>;

                _ ->
                    Acc
            end end).

-file("src/aws/internal/codec/xml_decode.gleam", 140).
?DOC(" Lookup an attribute by name on an element.\n").
-spec attr(element(), binary()) -> gleam@option:option(binary()).
attr(E, Name) ->
    case gleam@list:find(
        erlang:element(3, E),
        fun(P) -> erlang:element(1, P) =:= Name end
    ) of
        {ok, {_, V}} ->
            {some, V};

        {error, _} ->
            none
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 149).
-spec string_text(element()) -> {ok, binary()} | {error, binary()}.
string_text(E) ->
    {ok, text_content(E)}.

-file("src/aws/internal/codec/xml_decode.gleam", 153).
-spec bool_text(element()) -> {ok, boolean()} | {error, binary()}.
bool_text(E) ->
    case gleam@string:trim(text_content(E)) of
        <<"true"/utf8>> ->
            {ok, true};

        <<"false"/utf8>> ->
            {ok, false};

        Other ->
            {error, <<"xml: invalid bool: "/utf8, Other/binary>>}
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 161).
-spec int_text(element()) -> {ok, integer()} | {error, binary()}.
int_text(E) ->
    T = gleam@string:trim(text_content(E)),
    case gleam_stdlib:parse_int(T) of
        {ok, N} ->
            {ok, N};

        {error, _} ->
            {error, <<"xml: invalid int: "/utf8, T/binary>>}
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 169).
-spec float_text(element()) -> {ok, float()} | {error, binary()}.
float_text(E) ->
    T = gleam@string:trim(text_content(E)),
    case gleam_stdlib:parse_float(T) of
        {ok, F} ->
            {ok, F};

        {error, _} ->
            case gleam_stdlib:parse_int(T) of
                {ok, N} ->
                    {ok, erlang:float(N)};

                {error, _} ->
                    {error, <<"xml: invalid float: "/utf8, T/binary>>}
            end
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 188).
?DOC(
    " Like `float_text` but recognises the Smithy IEEE-754 special-\n"
    " value tokens (`NaN` / `Infinity` / `-Infinity`) and surfaces\n"
    " them as `json_float.SmithyFloat` variants. Used by generated\n"
    " Float decoders so the typed output carries the special value\n"
    " rather than failing the entire decode.\n"
).
-spec smithy_float_text(element()) -> {ok,
        aws@internal@codec@json_float:smithy_float()} |
    {error, binary()}.
smithy_float_text(E) ->
    T = gleam@string:trim(text_content(E)),
    case T of
        <<"NaN"/utf8>> ->
            {ok, na_n};

        <<"Infinity"/utf8>> ->
            {ok, pos_infinity};

        <<"-Infinity"/utf8>> ->
            {ok, neg_infinity};

        _ ->
            case float_text(E) of
                {ok, F} ->
                    {ok, {float_value, F}};

                {error, R} ->
                    {error, R}
            end
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 207).
?DOC(
    " Decode a Smithy `@timestamp` element. AWS XML APIs serialise these\n"
    " as ISO 8601 (e.g. `2024-01-02T03:04:05.000Z`); our type walker\n"
    " surfaces timestamps as `Int` (epoch seconds), so we parse the text\n"
    " and convert. Falls back to plain integer parsing for the rare case\n"
    " where the wire form is already epoch seconds.\n"
).
-spec timestamp_text(element()) -> {ok, integer()} | {error, binary()}.
timestamp_text(E) ->
    T = gleam@string:trim(text_content(E)),
    _pipe = aws_ffi:parse_iso8601(T),
    _pipe@1 = gleam@result:lazy_or(
        _pipe,
        fun() -> aws_ffi:parse_http_date(T) end
    ),
    _pipe@2 = gleam@result:lazy_or(
        _pipe@1,
        fun() -> gleam_stdlib:parse_int(T) end
    ),
    gleam@result:map_error(
        _pipe@2,
        fun(_) -> <<"xml: invalid timestamp: "/utf8, T/binary>> end
    ).

-file("src/aws/internal/codec/xml_decode.gleam", 223).
?DOC(
    " Decode a Smithy `@timestamp` XML element into the precise\n"
    " `Timestamp` shape (seconds + nanoseconds). The FFI ISO 8601\n"
    " parser is currently whole-second precision so `nanoseconds`\n"
    " will be 0 — once the parser learns fractional seconds we\n"
    " flip that here without breaking the API.\n"
).
-spec timestamp_text_precise(element()) -> {ok,
        aws@internal@codec@json_timestamp:timestamp()} |
    {error, binary()}.
timestamp_text_precise(E) ->
    case timestamp_text(E) of
        {ok, Secs} ->
            {ok, {timestamp, Secs, 0}};

        {error, Msg} ->
            {error, Msg}
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 242).
?DOC(
    " Decode an optional child element if present, otherwise return None.\n"
    " Used in the generated `decode_<struct>_xml_inner` for member fields.\n"
).
-spec optional_child(
    element(),
    binary(),
    fun((element()) -> {ok, NHZ} | {error, binary()})
) -> {ok, gleam@option:option(NHZ)} | {error, binary()}.
optional_child(Parent, Name, Decode) ->
    case find_child(Parent, Name) of
        none ->
            {ok, none};

        {some, E} ->
            gleam@result:map(Decode(E), fun(Field@0) -> {some, Field@0} end)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 255).
?DOC(
    " Decode a required child element, returning an error when the\n"
    " element is absent.\n"
).
-spec required_child(
    element(),
    binary(),
    fun((element()) -> {ok, NIF} | {error, binary()})
) -> {ok, NIF} | {error, binary()}.
required_child(Parent, Name, Decode) ->
    case find_child(Parent, Name) of
        none ->
            {error, <<"xml: missing required child: "/utf8, Name/binary>>};

        {some, E} ->
            Decode(E)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 268).
?DOC(
    " Decode a wrapped list: `<Wrapper><member>v</member>...</Wrapper>`.\n"
    " `wrapper` is the parent name, `member_name` is the per-entry tag.\n"
).
-spec optional_list(
    element(),
    binary(),
    binary(),
    fun((element()) -> {ok, NIK} | {error, binary()})
) -> {ok, gleam@option:option(list(NIK))} | {error, binary()}.
optional_list(Parent, Wrapper, Member_name, Decode) ->
    case find_child(Parent, Wrapper) of
        none ->
            {ok, none};

        {some, W} ->
            Entries = find_children(W, Member_name),
            _pipe = gleam@list:try_map(Entries, Decode),
            gleam@result:map(_pipe, fun(Field@0) -> {some, Field@0} end)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 284).
?DOC(" Decode a required wrapped list.\n").
-spec required_list(
    element(),
    binary(),
    binary(),
    fun((element()) -> {ok, NIR} | {error, binary()})
) -> {ok, list(NIR)} | {error, binary()}.
required_list(Parent, Wrapper, Member_name, Decode) ->
    case find_child(Parent, Wrapper) of
        none ->
            {error, <<"xml: missing required list: "/utf8, Wrapper/binary>>};

        {some, W} ->
            Entries = find_children(W, Member_name),
            gleam@list:try_map(Entries, Decode)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 302).
?DOC(
    " Decode a flattened list: each entry is a direct child of the\n"
    " parent (no wrapping element). Returns None when there are zero\n"
    " entries, matching the Option(List(a)) shape of normal lists.\n"
).
-spec optional_flat_list(
    element(),
    binary(),
    fun((element()) -> {ok, NIX} | {error, binary()})
) -> {ok, gleam@option:option(list(NIX))} | {error, binary()}.
optional_flat_list(Parent, Name, Decode) ->
    case find_children(Parent, Name) of
        [] ->
            {ok, none};

        Entries ->
            _pipe = gleam@list:try_map(Entries, Decode),
            gleam@result:map(_pipe, fun(Field@0) -> {some, Field@0} end)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 314).
?DOC(" Decode a required flattened list.\n").
-spec required_flat_list(
    element(),
    binary(),
    fun((element()) -> {ok, NJE} | {error, binary()})
) -> {ok, list(NJE)} | {error, binary()}.
required_flat_list(Parent, Name, Decode) ->
    case find_children(Parent, Name) of
        [] ->
            {error, <<"xml: missing required list: "/utf8, Name/binary>>};

        Entries ->
            gleam@list:try_map(Entries, Decode)
    end.

-file("src/aws/internal/codec/xml_decode.gleam", 330).
?DOC(
    " Decode the *inner* portion of a list element — used for nested\n"
    " lists where the outer caller has already wrapped each entry in\n"
    " `<member>...</member>` and we need to extract its children as a\n"
    " sub-list. Returns a bare `List(a)` (not optional) since the\n"
    " surrounding `optional_list` already gates on presence.\n"
).
-spec inner_list(
    element(),
    binary(),
    fun((element()) -> {ok, NJK} | {error, binary()})
) -> {ok, list(NJK)} | {error, binary()}.
inner_list(Elem, Member_name, Decode) ->
    gleam@list:try_map(find_children(Elem, Member_name), Decode).