aboutsummaryrefslogtreecommitdiff
path: root/src/xdata_codec.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/xdata_codec.erl')
-rw-r--r--src/xdata_codec.erl648
1 files changed, 648 insertions, 0 deletions
diff --git a/src/xdata_codec.erl b/src/xdata_codec.erl
new file mode 100644
index 000000000..223f24a1b
--- /dev/null
+++ b/src/xdata_codec.erl
@@ -0,0 +1,648 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 27 Sep 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(xdata_codec).
+
+%% API
+-export([compile/1, compile/2]).
+-export([dec_int/1, dec_int/3, dec_enum/2, dec_bool/1, not_empty/1,
+ dec_enum_int/2, dec_enum_int/4, enc_int/1, enc_enum/1,
+ enc_bool/1, enc_enum_int/1, format_error/1, enc_jid/1, dec_jid/1]).
+-include("xmpp.hrl").
+
+-record(state, {mod_name :: atom(),
+ file_name :: string(),
+ erl = "" :: string(),
+ hrl = "" :: string(),
+ dir = "" :: string(),
+ ns = <<>> :: binary(),
+ doc = <<>> :: binary(),
+ erl_dir = "" :: string(),
+ hrl_dir = "" :: string(),
+ prefix = [] :: [binary()],
+ dec_mfas = [] :: [{binary(), mfa()}],
+ enc_mfas = [] :: [{binary(), mfa()}],
+ specs = [] :: [{binary(), string()}],
+ required = [] :: [{binary(), boolean()} | binary()],
+ defaults = [] :: [{binary(), any()}]}).
+
+-define(is_multi_type(T),
+ ((T == 'list-multi') or (T == 'jid-multi') or (T == 'text-multi'))).
+
+-define(is_list_type(T),
+ ((T == 'list-single') or (T == 'list-multi'))).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+compile(Path) ->
+ compile(Path, []).
+
+compile(Path, Opts) ->
+ case filelib:is_dir(Path) of
+ true ->
+ filelib:fold_files(
+ Path, ".*.xdata", false,
+ fun(File, ok) ->
+ compile_file(File, Opts);
+ (_, Err) ->
+ Err
+ end, ok);
+ false ->
+ compile_file(Path, Opts)
+ end.
+
+compile_file(Path, Opts) ->
+ try
+ ok = application:ensure_started(fast_xml),
+ DirName = filename:dirname(Path),
+ FileName = filename:basename(Path),
+ RootName = filename:rootname(FileName),
+ ConfigPath = filename:join(DirName, RootName) ++ ".cfg",
+ ModName = list_to_atom(RootName),
+ {ok, Data} = file:read_file(Path),
+ Config = case file:consult(ConfigPath) of
+ {ok, Terms} -> lists:flatten(Terms);
+ {error, enoent} -> []
+ end,
+ State = #state{mod_name = ModName,
+ file_name = FileName,
+ erl = filename:rootname(FileName) ++ ".erl",
+ hrl = filename:rootname(FileName) ++ ".hrl",
+ dir = DirName,
+ prefix = proplists:get_all_values(prefix, Config),
+ erl_dir = proplists:get_value(erl_dir, Opts, DirName),
+ hrl_dir = proplists:get_value(hrl_dir, Opts, DirName),
+ dec_mfas = proplists:get_value(decode, Config, []),
+ enc_mfas = proplists:get_value(encode, Config, []),
+ specs = proplists:get_value(specs, Config, []),
+ required = proplists:get_value(required, Config, []),
+ defaults = proplists:get_value(defaults, Config, [])},
+ #xmlel{} = El = fxml_stream:parse_element(Data),
+ ok = compile_element(normalize(El), State),
+ io:format("Compiled ~s~n", [Path])
+ catch _:{badmatch, Err} ->
+ io:format(standard_error, "Failed to compile ~s: ~p~n",
+ [Path, Err]),
+ Err
+ end.
+
+emit(Format) ->
+ emit(Format, []).
+
+emit(Format, Args) ->
+ put(outbuf, get(outbuf) ++ io_lib:format(Format, Args)).
+
+dec_int(Val) ->
+ dec_int(Val, infinity, infinity).
+
+dec_int(Val, Min, Max) ->
+ case list_to_integer(binary_to_list(Val)) of
+ Int when Int =< Max, Min == infinity ->
+ Int;
+ Int when Int =< Max, Int >= Min ->
+ Int
+ end.
+
+enc_int(Int) ->
+ integer_to_binary(Int).
+
+dec_enum(Val, Enums) ->
+ AtomVal = erlang:binary_to_existing_atom(Val, utf8),
+ case lists:member(AtomVal, Enums) of
+ true ->
+ AtomVal
+ end.
+
+enc_enum(Atom) ->
+ erlang:atom_to_binary(Atom, utf8).
+
+dec_enum_int(Val, Enums) ->
+ try dec_int(Val)
+ catch _:_ -> dec_enum(Val, Enums)
+ end.
+
+dec_enum_int(Val, Enums, Min, Max) ->
+ try dec_int(Val, Min, Max)
+ catch _:_ -> dec_enum(Val, Enums)
+ end.
+
+enc_enum_int(Int) when is_integer(Int) ->
+ enc_int(Int);
+enc_enum_int(Atom) ->
+ enc_enum(Atom).
+
+dec_bool(<<"1">>) -> true;
+dec_bool(<<"0">>) -> false;
+dec_bool(<<"true">>) -> true;
+dec_bool(<<"false">>) -> false.
+
+enc_bool(true) -> <<"1">>;
+enc_bool(false) -> <<"0">>.
+
+enc_jid(J) -> jid:to_string(J).
+
+dec_jid(Val) ->
+ case jid:from_string(Val) of
+ error -> erlang:error(badarg);
+ J -> J
+ end.
+
+not_empty(<<_, _/binary>> = Val) ->
+ Val.
+
+format_error({form_type_mismatch, Type}) ->
+ <<"FORM_TYPE doesn't match '", Type/binary, "'">>;
+format_error({bad_var_value, Var, Type}) ->
+ <<"Bad value of field '", Var/binary, "' of type '", Type/binary, "'">>;
+format_error({missing_value, Var, Type}) ->
+ <<"Missing value of field '", Var/binary, "' of type '", Type/binary, "'">>;
+format_error({too_many_values, Var, Type}) ->
+ <<"Too many values for field '", Var/binary, "' of type '", Type/binary, "'">>;
+format_error({unknown_var, Var, Type}) ->
+ <<"Unknown field '", Var/binary, "' of type '", Type/binary, "'">>;
+format_error({missing_required_var, Var, Type}) ->
+ <<"Missing required field '", Var/binary, "' of type '", Type/binary, "'">>.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+compile_element(#xmlel{name = <<"form_type">>, children = Els} = Form,
+ #state{erl = OutErl, erl_dir = ErlDir,
+ hrl = OutHrl, hrl_dir = HrlDir} = State0) ->
+ try
+ Name = fxml:get_subtag_cdata(Form, <<"name">>),
+ Doc = fxml:get_subtag_cdata(Form, <<"doc">>),
+ X = #xmlel{name = <<"x">>,
+ attrs = [{<<"type">>, <<"form">>},
+ {<<"xmlns">>, <<"jabber:x:data">>}],
+ children = Els},
+ State = State0#state{ns = Name, doc = Doc},
+ #xdata{fields = Fs} = xmpp_codec:decode(X),
+ put(outbuf, []),
+ mk_header(State),
+ mk_aux_funs(),
+ mk_top_decoder(Fs, State),
+ mk_top_encoder(Fs, State),
+ mk_decoder(Fs, State),
+ mk_encoders(Fs, State),
+ ErlData = get(outbuf),
+ ok = file:write_file(filename:join(ErlDir, OutErl), ErlData),
+ ok = erl_tidy:file(filename:join(ErlDir, OutErl), [{backups, false}]),
+ put(outbuf, []),
+ mk_type_definitions(Fs, State),
+ HrlData = get(outbuf),
+ ok = file:write_file(filename:join(HrlDir, OutHrl), HrlData)
+ catch _:{badmatch, Err} ->
+ Err
+ end.
+
+mk_aux_funs() ->
+ case get_abstract_code_from_myself() of
+ {ok, AbsCode} ->
+ AST = lists:filter(
+ fun(T) ->
+ case catch erl_syntax_lib:analyze_function(T) of
+ {format_error, 1} -> true;
+ {dec_int, 3} -> true;
+ {dec_int, 1} -> true;
+ {dec_enum, 2} -> true;
+ {dec_enum_int, 2} -> true;
+ {dec_enum_int, 4} -> true;
+ {enc_int, 1} -> true;
+ {enc_enum, 1} -> true;
+ {enc_enum_int, 1} -> true;
+ {not_empty, 1} -> true;
+ {dec_bool, 1} -> true;
+ {enc_bool, 1} -> true;
+ {enc_jid, 1} -> true;
+ {dec_jid, 1} -> true;
+ _ -> false
+ end
+ end, AbsCode),
+ emit(erl_prettypr:format(erl_syntax:form_list(AST)) ++ io_lib:nl());
+ error ->
+ erlang:error({no_abstract_code_found, ?MODULE})
+ end.
+
+get_abstract_code_from_myself() ->
+ {file, File} = code:is_loaded(?MODULE),
+ case beam_lib:chunks(File, [abstract_code]) of
+ {ok, {_, List}} ->
+ case lists:keyfind(abstract_code, 1, List) of
+ {abstract_code, {raw_abstract_v1, Abstr}} ->
+ {ok, Abstr};
+ _ ->
+ error
+ end;
+ _ ->
+ error
+ end.
+
+mk_comment_header(#state{file_name = Source, ns = NS, doc = Doc}) ->
+ emit("%% Created automatically by xdata generator (xdata_codec.erl)~n"
+ "%% Source: ~s~n"
+ "%% Form type: ~s~n", [Source, NS]),
+ if Doc /= <<>> -> emit("%% Document: ~s~n~n", [Doc]);
+ true -> emit("~n")
+ end.
+
+mk_header(#state{mod_name = Mod, hrl = Include} = State) ->
+ mk_comment_header(State),
+ emit("~n-module(~s).~n", [Mod]),
+ emit("-export([decode/1, decode/2, encode/1, encode/2, format_error/1]).~n"),
+ emit("-include(\"xmpp_codec.hrl\").~n"),
+ emit("-include(\"~s\").~n", [Include]),
+ emit("-export_type([property/0, result/0, form/0]).~n").
+
+mk_type_definitions(Fs, State) ->
+ mk_comment_header(State),
+ lists:foreach(
+ fun(#xdata_field{var = Var} = F) ->
+ Spec = get_typespec(F, State),
+ case is_complex_type(Spec) of
+ true ->
+ emit("-type '~s'() :: ~s.~n",
+ [var_to_rec_field(Var, State), Spec]);
+ false ->
+ ok
+ end
+ end, Fs),
+ emit("~n-type property() :: "),
+ Fields = lists:map(
+ fun(#xdata_field{var = Var} = F) ->
+ RecField = var_to_rec_field(Var, State),
+ [io_lib:format("{'~s', ~s}",
+ [RecField, mk_typespec(F, State)])]
+ end, Fs),
+ emit(string:join(Fields, " |~n ") ++ ".~n"),
+ emit("-type result() :: [property()].~n~n"),
+ VarsWithSpec = lists:flatmap(
+ fun(#xdata_field{type = T, var = Var} = F)
+ when ?is_list_type(T) ->
+ RecName = var_to_rec_field(Var, State),
+ Spec0 = get_typespec(F, State),
+ Spec = case is_complex_type(Spec0) of
+ true ->
+ io_lib:format("'~s'()", [RecName]);
+ false ->
+ Spec0
+ end,
+ [{RecName, mk_typespec(F, State), Spec}];
+ (_) ->
+ []
+ end, Fs),
+ case VarsWithSpec of
+ [] ->
+ emit("-type form() :: [property() | xdata_field()].~n");
+ _ ->
+ emit("-type options(T) :: [{binary(), T}].~n"),
+ emit("-type property_with_options() ::~n "),
+ Options = [io_lib:format("{'~s', ~s, options(~s)}",
+ [Var, Spec1, Spec2])
+ || {Var, Spec1, Spec2} <- VarsWithSpec],
+ emit(string:join(Options, " |~n ") ++ ".~n"),
+ emit("-type form() :: [property() | property_with_options() | xdata_field()].~n")
+ end.
+
+mk_top_decoder(Fs, State) ->
+ Required = [Var || #xdata_field{var = Var} <- Fs, is_required(Var, State)],
+ emit("decode(Fs) -> decode(Fs, []).~n"),
+ emit("decode(Fs, Acc) ->"
+ " case lists:keyfind(<<\"FORM_TYPE\">>, #xdata_field.var, Fs) of"
+ " false ->"
+ " decode(Fs, Acc, ~p);"
+ " #xdata_field{values = [~p]} ->"
+ " decode(Fs, Acc, ~p);"
+ " _ ->"
+ " erlang:error({?MODULE, {form_type_mismatch, ~p}})~n"
+ " end.~n",
+ [Required, State#state.ns, Required, State#state.ns]).
+
+mk_top_encoder(Fs, State) ->
+ Clauses = string:join(
+ lists:map(
+ fun(#xdata_field{var = Var, type = T}) when ?is_list_type(T) ->
+ Field = var_to_rec_field(Var, State),
+ io_lib:format(
+ "{'~s', Val} -> ['encode_~s'(Val, default, Translate)];"
+ "{'~s', Val, Opts} -> ['encode_~s'(Val, Opts, Translate)]",
+ [Field, Field, Field, Field]);
+ (#xdata_field{var = Var}) ->
+ Field = var_to_rec_field(Var, State),
+ io_lib:format(
+ "{'~s', Val} -> ['encode_~s'(Val, Translate)];"
+ "{'~s', _, _} -> erlang:error({badarg, Opt})",
+ [Field, Field, Field])
+ end, Fs) ++ ["#xdata_field{} -> [Opt]; _ -> []"],
+ ";"),
+ emit("encode(Cfg) -> encode(Cfg, fun(Text) -> Text end).~n"),
+ emit("encode(List, Translate) when is_list(List) ->"
+ " Fs = [case Opt of ~s end || Opt <- List],"
+ " FormType = #xdata_field{var = <<\"FORM_TYPE\">>, type = hidden,"
+ " values = [~p]},"
+ " [FormType|lists:flatten(Fs)].~n",
+ [Clauses, State#state.ns]).
+
+mk_decoder([#xdata_field{var = Var, type = Type} = F|Fs], State) ->
+ ValVar = if ?is_multi_type(Type) -> "Values";
+ true -> "[Value]"
+ end,
+ DecFun = if ?is_multi_type(Type) ->
+ ["[", mk_decoding_fun(F, State), " || Value <- Values]"];
+ true ->
+ mk_decoding_fun(F, State)
+ end,
+ emit("decode([#xdata_field{var = ~p, values = ~s}|Fs], Acc, Required) ->"
+ " try ~s of"
+ " Result -> decode(Fs, [{'~s', Result}|Acc],"
+ " lists:delete(~p, Required))"
+ " catch _:_ ->"
+ " erlang:error({?MODULE, {bad_var_value, ~p, ~p}})"
+ " end;",
+ [Var, ValVar, DecFun, var_to_rec_field(Var, State),
+ Var, Var, State#state.ns]),
+ if not ?is_multi_type(Type) ->
+ emit("decode([#xdata_field{var = ~p, values = []} = F|Fs],"
+ " Acc, Required) ->"
+ " decode([F#xdata_field{var = ~p, values = [<<>>]}|Fs],"
+ " Acc, Required);",
+ [Var, Var]),
+ emit("decode([#xdata_field{var = ~p}|_], _, _) ->"
+ " erlang:error({?MODULE, {too_many_values, ~p, ~p}});",
+ [Var, Var, State#state.ns]);
+ true ->
+ ok
+ end,
+ mk_decoder(Fs, State);
+mk_decoder([], State) ->
+ emit("decode([#xdata_field{var = Var}|Fs], Acc, Required) ->"
+ " if Var /= <<\"FORM_TYPE\">> ->"
+ " erlang:error({?MODULE, {unknown_var, Var, ~p}});"
+ " true ->"
+ " decode(Fs, Acc, Required)"
+ " end;",
+ [State#state.ns]),
+ emit("decode([], _, [Var|_]) ->"
+ " erlang:error({?MODULE, {missing_required_var, Var, ~p}});~n",
+ [State#state.ns]),
+ emit("decode([], Acc, []) -> Acc.~n").
+
+mk_encoders(Fs, State) ->
+ lists:foreach(
+ fun(#xdata_field{var = Var, required = IsRequired, desc = Desc,
+ label = Label, type = Type} = F) ->
+ EncVals = mk_encoded_values(F, State),
+ EncOpts = mk_encoded_options(F, State),
+ FieldName = var_to_rec_field(Var, State),
+ DescStr = if Desc == <<>> -> "<<>>";
+ true -> io_lib:format("Translate(~p)", [Desc])
+ end,
+ LabelStr = if Label == <<>> -> "<<>>";
+ true -> io_lib:format("Translate(~p)", [Label])
+ end,
+ if ?is_list_type(Type) ->
+ emit("'encode_~s'(Value, Options, Translate) ->", [FieldName]);
+ true ->
+ emit("'encode_~s'(Value, Translate) ->", [FieldName])
+ end,
+ emit(" Values = ~s,"
+ " Opts = ~s,"
+ " #xdata_field{var = ~p,"
+ " values = Values,"
+ " required = ~p,"
+ " type = ~p,"
+ " options = Opts,"
+ " desc = ~s,"
+ " label = ~s}.~n",
+ [EncVals, EncOpts, Var, IsRequired, Type, DescStr, LabelStr])
+ end, Fs).
+
+mk_encoded_values(#xdata_field{var = Var, type = Type,
+ options = Options}, State) ->
+ EncFun =
+ case get_enc_fun(Var, Type, Options, State) of
+ {M, Fun, Args} ->
+ Mod = if M == undefined -> "";
+ true -> io_lib:format("~s:", [M])
+ end,
+ FArgs = [io_lib:format(", ~p", [A]) || A <- Args],
+ if ?is_multi_type(Type) ->
+ "[" ++ io_lib:format("~s~s(V~s)", [Mod, Fun, FArgs]) ++
+ " || V <- Value]";
+ true ->
+ "[" ++ io_lib:format("~s~s(Value~s)", [Mod, Fun, FArgs])
+ ++ "]"
+ end;
+ undefined ->
+ "[Value]"
+ end,
+ Default = case get_dec_fun(Var, Type, Options, State) of
+ _ when ?is_multi_type(Type) -> "[]";
+ undefined -> "<<>>";
+ _MFA -> "undefined"
+ end,
+ io_lib:format(
+ "case Value of"
+ " ~s -> [];~n"
+ " Value -> ~s~n"
+ "end",
+ [Default, EncFun]).
+
+mk_encoded_options(#xdata_field{var = Var, type = Type,
+ options = Options}, State) ->
+ EncFun = case get_enc_fun(Var, Type, Options, State) of
+ {M, Fun, Args} ->
+ Mod = if M == undefined -> "";
+ true -> io_lib:format("~s:", [M])
+ end,
+ FArgs = [io_lib:format(", ~p", [A]) || A <- Args],
+ io_lib:format("~s~s(V~s)", [Mod, Fun, FArgs]);
+ undefined ->
+ "V"
+ end,
+ EncOpts = string:join(
+ [case L of
+ <<>> ->
+ io_lib:format("#xdata_option{value = ~p}", [V]);
+ _ ->
+ io_lib:format(
+ "#xdata_option{label = Translate(~p), value = ~p}",
+ [L, V])
+ end || #xdata_option{label = L, value = V} <- Options],
+ ","),
+ if ?is_list_type(Type) ->
+ io_lib:format(
+ "if Options == default ->"
+ " [~s];"
+ "true ->"
+ " [#xdata_option{label = Translate(L), value = ~s}"
+ " || {L, V} <- Options]"
+ "end",
+ [EncOpts, EncFun]);
+ true ->
+ "[]"
+ end.
+
+mk_decoding_fun(#xdata_field{var = Var, type = Type,
+ options = Options}, State) ->
+ case get_dec_fun(Var, Type, Options, State) of
+ {M, Fun, Args} ->
+ Mod = if M == undefined -> "";
+ true -> io_lib:format("~s:", [M])
+ end,
+ FArgs = [io_lib:format(", ~p", [A]) || A <- Args],
+ io_lib:format("~s~s(Value~s)", [Mod, Fun, FArgs]);
+ undefined ->
+ "Value"
+ end.
+
+var_to_rec_field(Var, #state{prefix = [Prefix|T]} = State) ->
+ Size = size(Prefix),
+ case Var of
+ <<(Prefix):Size/binary, Rest/binary>> ->
+ binary_to_atom(Rest, utf8);
+ _ ->
+ var_to_rec_field(Var, State#state{prefix = T})
+ end;
+var_to_rec_field(Var, #state{prefix = []}) ->
+ Var.
+
+get_dec_fun(Var, Type, Options, State) ->
+ case lists:keyfind(Var, 1, State#state.dec_mfas) of
+ false when Type == 'list-multi'; Type == 'list-single' ->
+ if Options /= [] ->
+ Variants = [binary_to_atom(V, utf8)
+ || #xdata_option{value = V} <- Options],
+ {undefined, dec_enum, [Variants]};
+ true ->
+ undefined
+ end;
+ false when Type == 'jid-multi'; Type == 'jid-single' ->
+ {undefined, dec_jid, []};
+ false when Type == boolean ->
+ {undefined, dec_bool, []};
+ false ->
+ undefined;
+ {Var, {M, F, A}} ->
+ {M, F, A};
+ {Var, {dec_bool, []}} ->
+ {undefined, dec_bool, []};
+ {Var, {not_empty, []}} ->
+ {undefined, not_empty, []};
+ {Var, {dec_enum, [Variants]}} ->
+ {undefined, dec_enum, [Variants]};
+ {Var, {dec_int, Args}} ->
+ {undefined, dec_int, Args};
+ {Var, {dec_enum_int, Args}} ->
+ {undefined, dec_enum_int, Args};
+ {Var, {dec_jid, []}} ->
+ {undefined, dec_jid, []}
+ end.
+
+get_enc_fun(Var, Type, Options, State) ->
+ case get_dec_fun(Var, Type, Options, State) of
+ {undefined, dec_enum, _} ->
+ {undefined, enc_enum, []};
+ {undefined, dec_bool, _} ->
+ {undefined, enc_bool, []};
+ {undefined, dec_int, _} ->
+ {undefined, enc_int, []};
+ {undefined, dec_enum_int, _} ->
+ {undefined, enc_enum_int, []};
+ {undefined, dec_jid, _} ->
+ {undefined, enc_jid, []};
+ _ ->
+ case lists:keyfind(Var, 1, State#state.enc_mfas) of
+ false ->
+ undefined;
+ {Var, {M, F, A}} ->
+ {M, F, A};
+ {Var, {enc_bool, []}} ->
+ {undefined, enc_bool, []};
+ {Var, {dec_enum, _}} ->
+ {undefined, enc_enum, []};
+ {Var, {enc_int, _}} ->
+ {undefined, enc_int, []};
+ {Var, {dec_enum_int, _}} ->
+ {undefined, enc_enum_int, []};
+ {Var, {enc_jid, _}} ->
+ {undefined, enc_jid, []}
+ end
+ end.
+
+mk_typespec(#xdata_field{type = Type, var = Var} = Field, State) ->
+ Spec0 = get_typespec(Field, State),
+ Spec1 = case is_complex_type(Spec0) of
+ true ->
+ io_lib:format("'~s'()", [var_to_rec_field(Var, State)]);
+ false ->
+ Spec0
+ end,
+ if ?is_multi_type(Type) -> "[" ++ Spec1 ++ "]";
+ true -> Spec1
+ end.
+
+get_typespec(#xdata_field{var = Var, type = Type, options = Options}, State) ->
+ case lists:keyfind(Var, 1, State#state.specs) of
+ false ->
+ case get_dec_fun(Var, Type, Options, State) of
+ {undefined, dec_enum, Args} ->
+ enum_spec(Args);
+ {undefined, dec_bool, _} ->
+ "boolean()";
+ {undefined, dec_jid, _} ->
+ "jid:jid()";
+ {undefined, dec_int, Args} ->
+ int_spec(Args);
+ {undefined, dec_enum_int, [Variants|T]} ->
+ enum_spec([Variants]) ++ " | " ++ int_spec(T);
+ _ ->
+ "binary()"
+ end;
+ {Var, Spec} ->
+ Spec
+ end.
+
+-spec is_complex_type(string()) -> boolean().
+is_complex_type(Spec) ->
+ string:chr(Spec, $|) /= 0.
+
+int_spec([]) ->
+ "integer()";
+int_spec([From, To]) ->
+ if From /= infinity, To /= infinity ->
+ io_lib:format("~p..~p", [From, To]);
+ From > 0 ->
+ "pos_integer()";
+ From == 0 ->
+ "non_neg_integer()";
+ true ->
+ "integer()"
+ end.
+
+enum_spec([Variants]) ->
+ string:join([atom_to_list(V) || V <- Variants], " | ").
+
+is_required(Var, State) ->
+ lists:member(Var, State#state.required) orelse
+ proplists:get_bool(Var, State#state.required).
+
+normalize(#xmlel{name = Name, attrs = Attrs, children = Els}) ->
+ #xmlel{name = Name,
+ attrs = [normalize(Attr) || Attr <- Attrs],
+ children = [normalize(El) || El <- Els]};
+normalize({Key, Data}) ->
+ {Key, normalize(Data)};
+normalize(Txt) when is_binary(Txt) ->
+ case re:split(Txt, "[\\s\\r\\n\\t]+", [trim, {return, list}]) of
+ [""|T] ->
+ list_to_binary(string:join(T, " "));
+ T ->
+ list_to_binary(string:join(T, " "))
+ end.