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