1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : pubsub_form_utils.erl |
3 |
|
%%% Author : Piotr Nosek <piotr.nosek@erlang-solutions.com> |
4 |
|
%%% Purpose : mod_pubsub form processing utilities |
5 |
|
%%% Created : 28 Nov 2018 by Piotr Nosek <piotr.nosek@erlang-solutions.com> |
6 |
|
|
7 |
|
%%% Portions created by ProcessOne and Brian Cully <bjc@kublai.com> |
8 |
|
%%%---------------------------------------------------------------------- |
9 |
|
|
10 |
|
-module(pubsub_form_utils). |
11 |
|
|
12 |
|
-author("piotr.nosek@erlang-solutions.com"). |
13 |
|
|
14 |
|
-export([make_sub_xform/1, parse_sub_xform/1]). |
15 |
|
|
16 |
|
-include("mongoose_logger.hrl"). |
17 |
|
-include("mongoose_ns.hrl"). |
18 |
|
-include_lib("exml/include/exml.hrl"). |
19 |
|
|
20 |
|
-type convert_from_binary_fun() :: fun(([binary()]) -> any()). |
21 |
|
-type convert_to_binary_fun() :: fun((any()) -> [binary()]). |
22 |
|
-type convert_funs() :: #{ from_binaries := convert_from_binary_fun(), |
23 |
|
to_binaries := convert_to_binary_fun() }. |
24 |
|
-type field_data_type() :: boolean | integer | datetime | list | atom | {custom, convert_funs()}. |
25 |
|
|
26 |
|
-type option_properties() :: #{ |
27 |
|
form_type => binary(), % type reported as field attr in XML |
28 |
|
possible_choices => [{Value :: binary(), ValueDescription :: binary()}], |
29 |
|
data_type => field_data_type(), |
30 |
|
label => binary() |
31 |
|
}. |
32 |
|
|
33 |
|
-type option_definition() :: {FormVar :: binary(), |
34 |
|
InternalName :: atom(), |
35 |
|
OptionProperties :: option_properties()}. |
36 |
|
|
37 |
|
-type convert_from_binary_error() :: {error, {unknown_option, VarName :: binary}}. |
38 |
|
-type parse_error() :: {error, invalid_form} | convert_from_binary_error(). |
39 |
|
|
40 |
|
%%==================================================================== |
41 |
|
%% API |
42 |
|
%%==================================================================== |
43 |
|
|
44 |
|
%% Missing options won't have any <value/> elements |
45 |
|
%% TODO: Right now |
46 |
|
-spec make_sub_xform(Options :: mod_pubsub:subOptions()) -> {ok, exml:element()}. |
47 |
|
make_sub_xform(Options) -> |
48 |
8 |
XFields = [make_field_xml(OptDefinition, Options) || OptDefinition <- sub_form_options()], |
49 |
8 |
{ok, make_sub_xform_xml(XFields)}. |
50 |
|
|
51 |
|
%% The list of options returned by this function may be a subset of the options schema. |
52 |
|
%% TODO: It is the behaviour of original code. Maybe it should be changed? To discuss. |
53 |
|
-spec parse_sub_xform(exml:element() | undefined) -> {ok, mod_pubsub:subOptions()} | parse_error(). |
54 |
|
parse_sub_xform(undefined) -> |
55 |
81 |
{ok, []}; |
56 |
|
parse_sub_xform(XForm) -> |
57 |
14 |
case jlib:parse_xdata_submit(XForm) of |
58 |
:-( |
invalid -> {error, invalid_form}; |
59 |
14 |
XData -> convert_fields_from_binaries(XData, [], sub_form_options()) |
60 |
|
end. |
61 |
|
|
62 |
|
%%==================================================================== |
63 |
|
%% Form XML builders |
64 |
|
%%==================================================================== |
65 |
|
|
66 |
|
-spec make_sub_xform_xml(XFields :: [exml:element()]) -> exml:element(). |
67 |
|
make_sub_xform_xml(XFields) -> |
68 |
8 |
FormTypeEl = #xmlel{name = <<"field">>, |
69 |
|
attrs = [{<<"var">>, <<"FORM_TYPE">>}, {<<"type">>, <<"hidden">>}], |
70 |
|
children = [#xmlel{name = <<"value">>, attrs = [], |
71 |
|
children = [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}, |
72 |
8 |
#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}], children = [FormTypeEl | XFields]}. |
73 |
|
|
74 |
|
-spec make_field_xml(OptDefinition :: option_definition(), |
75 |
|
Options :: mod_pubsub:subOptions()) -> exml:element(). |
76 |
|
make_field_xml({VarName, Key, #{ label := Label, form_type := FormType } = VarProps}, Options) -> |
77 |
64 |
ChoicesEls = make_choices_xml(VarProps), |
78 |
64 |
ValEls = make_values_xml(Key, Options, VarProps), |
79 |
|
|
80 |
64 |
#xmlel{name = <<"field">>, |
81 |
|
attrs = [{<<"var">>, VarName}, {<<"type">>, FormType}, {<<"label">>, Label}], |
82 |
|
children = ChoicesEls ++ ValEls}. |
83 |
|
|
84 |
|
make_choices_xml(#{ possible_choices := PossibleChoices }) -> |
85 |
24 |
[ make_option_xml(Value, Label) || {Value, Label} <- PossibleChoices ]; |
86 |
|
make_choices_xml(#{}) -> |
87 |
40 |
[]. |
88 |
|
|
89 |
|
make_option_xml(Value, Label) -> |
90 |
72 |
#xmlel{name = <<"option">>, attrs = [{<<"label">>, Label}], children = [make_value_xml(Value)]}. |
91 |
|
|
92 |
|
make_values_xml(Key, Options, #{ data_type := DataType }) -> |
93 |
64 |
case lists:keyfind(Key, 1, Options) of |
94 |
|
{_, Value} -> |
95 |
8 |
[make_value_xml(BinVal) || BinVal <- convert_value_to_binaries(Value, DataType)]; |
96 |
|
false -> |
97 |
56 |
[] |
98 |
|
end. |
99 |
|
|
100 |
|
make_value_xml(Value) -> |
101 |
80 |
#xmlel{name = <<"value">>, attrs = [], children = [#xmlcdata{ content = Value }]}. |
102 |
|
|
103 |
|
%%==================================================================== |
104 |
|
%% Form definitions & conversions |
105 |
|
%%==================================================================== |
106 |
|
|
107 |
|
-spec sub_form_options() -> [option_definition()]. |
108 |
|
sub_form_options() -> |
109 |
22 |
[ |
110 |
|
{<<"pubsub#deliver">>, deliver, |
111 |
|
#{ form_type => <<"boolean">>, |
112 |
|
data_type => boolean, |
113 |
|
label => <<"Whether an entity wants to receive or disable notifications">> |
114 |
|
}}, |
115 |
|
|
116 |
|
{<<"pubsub#digest">>, digest, |
117 |
|
#{ form_type => <<"boolean">>, |
118 |
|
data_type => boolean, |
119 |
|
label => <<"Whether an entity wants to receive digests (aggregations)" |
120 |
|
" of notifications or all notifications individually">> |
121 |
|
}}, |
122 |
|
|
123 |
|
{<<"pubsub#digest_frequency">>, digest_frequency, |
124 |
|
#{ form_type => <<"text-single">>, |
125 |
|
data_type => integer, |
126 |
|
label => <<"The minimum number of milliseconds between sending" |
127 |
|
" any two notification digests">> |
128 |
|
}}, |
129 |
|
|
130 |
|
{<<"pubsub#expire">>, expire, |
131 |
|
#{ form_type => <<"text-single">>, |
132 |
|
data_type => datetime, |
133 |
|
label => <<"The DateTime at which a leased subscription will end or has ended">> |
134 |
|
}}, |
135 |
|
|
136 |
|
{<<"pubsub#include_body">>, include_body, |
137 |
|
#{ form_type => <<"boolean">>, |
138 |
|
data_type => boolean, |
139 |
|
label => <<"Whether an entity wants to receive an XMPP message body" |
140 |
|
" in addition to the payload format">> |
141 |
|
}}, |
142 |
|
|
143 |
|
{<<"pubsub#show-values">>, show_values, |
144 |
|
#{ form_type => <<"list-multi">>, |
145 |
|
possible_choices => [{<<"away">>, <<"Away">>}, |
146 |
|
{<<"chat">>, <<"Chat">>}, |
147 |
|
{<<"dnd">>, <<"Do Not Disturb">>}, |
148 |
|
{<<"online">>, <<"Any online state">>}, |
149 |
|
{<<"xa">>, <<"Extended Away">>}], |
150 |
|
data_type => list, |
151 |
|
label => <<"The presence states for which an entity wants to receive notifications">> |
152 |
|
}}, |
153 |
|
|
154 |
|
{<<"pubsub#subscription_type">>, subscription_type, |
155 |
|
#{ form_type => <<"list-single">>, |
156 |
|
possible_choices => [{<<"items">>, <<"Receive notification of new items only">>}, |
157 |
|
{<<"nodes">>, <<"Receive notification of new nodes only">>}], |
158 |
|
data_type => {custom, #{ from_binaries => fun convert_sub_type_from_binary/1, |
159 |
|
to_binaries => fun convert_sub_type_to_binary/1 }}, |
160 |
|
label => <<"Type of notification to receive">> |
161 |
|
}}, |
162 |
|
|
163 |
|
{<<"pubsub#subscription_depth">>, subscription_depth, |
164 |
|
#{ form_type => <<"list-single">>, |
165 |
|
possible_choices => [{<<"1">>, <<"Receive notification from direct child nodes only">>}, |
166 |
|
{<<"all">>, <<"Receive notification from all descendent nodes">>}], |
167 |
|
data_type => {custom, #{ from_binaries => fun convert_sub_depth_from_binary/1, |
168 |
|
to_binaries => fun convert_sub_depth_to_binary/1 }}, |
169 |
|
label => <<"Depth from subscription for which to receive notifications">> |
170 |
|
}} |
171 |
|
]. |
172 |
|
|
173 |
|
-spec convert_fields_from_binaries([{VarNameBin :: binary(), Values :: [binary()]}], |
174 |
|
Acc :: mod_pubsub:subOptions(), |
175 |
|
Schema :: [option_definition()]) -> |
176 |
|
{ok, mod_pubsub:subOptions()} | convert_from_binary_error(). |
177 |
|
convert_fields_from_binaries([], Result, _Schema) -> |
178 |
14 |
{ok, Result}; |
179 |
|
convert_fields_from_binaries([{<<"FORM_TYPE">>, _Values} | RData], Acc, Schema) -> |
180 |
14 |
convert_fields_from_binaries(RData, Acc, Schema); |
181 |
|
convert_fields_from_binaries([{VarBin, Values} | RData], Acc, Schema) -> |
182 |
14 |
case lists:keyfind(VarBin, 1, Schema) of |
183 |
|
{_VBin, _Var, #{ data_type := DataType }} when Values == [] andalso DataType /= list -> |
184 |
:-( |
convert_fields_from_binaries(RData, Acc, Schema); |
185 |
|
{_VBin, Var, #{ data_type := DataType }} -> |
186 |
14 |
try convert_value_from_binaries(Values, DataType) of |
187 |
|
Converted -> |
188 |
14 |
NAcc = lists:keystore(Var, 1, Acc, {Var, Converted}), |
189 |
14 |
convert_fields_from_binaries(RData, NAcc, Schema) |
190 |
|
catch |
191 |
|
C:R:S -> |
192 |
:-( |
{error, {conversion_failed, {Var, DataType, C, R, S}}} |
193 |
|
end; |
194 |
|
false -> |
195 |
:-( |
{error, {unknown_option, VarBin}} |
196 |
|
end. |
197 |
|
|
198 |
|
-spec convert_value_from_binaries(Bins :: [binary()], field_data_type()) -> any(). |
199 |
|
convert_value_from_binaries(Bins, {custom, #{ from_binaries := ConvertFromBinaryFun }}) -> |
200 |
:-( |
ConvertFromBinaryFun(Bins); |
201 |
|
convert_value_from_binaries([Bin], boolean) -> |
202 |
14 |
convert_bool_from_binary(Bin); |
203 |
|
convert_value_from_binaries([Bin], integer) -> |
204 |
:-( |
binary_to_integer(Bin); |
205 |
|
convert_value_from_binaries([Bin], datetime) -> |
206 |
:-( |
calendar:rfc3339_to_system_time(binary_to_list(Bin), [{unit, microsecond}]); |
207 |
|
convert_value_from_binaries(Bins, list) when is_list(Bins) -> |
208 |
:-( |
Bins. |
209 |
|
|
210 |
|
-spec convert_value_to_binaries(Value :: any(), field_data_type()) -> [binary()]. |
211 |
|
convert_value_to_binaries(Value, {custom, #{ to_binaries := ConvertToBinaryFun }}) -> |
212 |
:-( |
ConvertToBinaryFun(Value); |
213 |
|
convert_value_to_binaries(Value, boolean) -> |
214 |
8 |
[convert_bool_to_binary(Value)]; |
215 |
|
convert_value_to_binaries(Value, integer) -> |
216 |
:-( |
[integer_to_binary(Value)]; |
217 |
|
convert_value_to_binaries(Value, datetime) -> |
218 |
:-( |
TS = calendar:system_time_to_rfc3339(Value, [{offset, "Z"}, {unit, microsecond}]), |
219 |
:-( |
list_to_binary(TS); |
220 |
|
convert_value_to_binaries(Value, list) when is_list(Value) -> |
221 |
:-( |
Value. |
222 |
|
|
223 |
:-( |
convert_bool_from_binary(<<"0">>) -> false; |
224 |
:-( |
convert_bool_from_binary(<<"1">>) -> true; |
225 |
8 |
convert_bool_from_binary(<<"false">>) -> false; |
226 |
6 |
convert_bool_from_binary(<<"true">>) -> true. |
227 |
|
|
228 |
2 |
convert_bool_to_binary(true) -> <<"true">>; |
229 |
6 |
convert_bool_to_binary(false) -> <<"false">>. |
230 |
|
|
231 |
:-( |
convert_sub_depth_from_binary([<<"all">>]) -> all; |
232 |
:-( |
convert_sub_depth_from_binary([DepthBin]) -> binary_to_integer(DepthBin). |
233 |
|
|
234 |
:-( |
convert_sub_depth_to_binary(all) -> [<<"all">>]; |
235 |
:-( |
convert_sub_depth_to_binary(Depth) -> [integer_to_binary(Depth)]. |
236 |
|
|
237 |
:-( |
convert_sub_type_from_binary(<<"items">>) -> items; |
238 |
:-( |
convert_sub_type_from_binary(<<"nodes">>) -> nodes. |
239 |
|
|
240 |
:-( |
convert_sub_type_to_binary(items) -> <<"items">>; |
241 |
:-( |
convert_sub_type_to_binary(nodes) -> <<"nodes">>. |