xmpp.chapril.org-ejabberd/src/xdata_codec.erl

649 lines
20 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @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.