1%% @author Couchbase <info@couchbase.com>
2%% @copyright 2018 Couchbase, Inc.
3%%
4%% Licensed under the Apache License, Version 2.0 (the "License");
5%% you may not use this file except in compliance with the License.
6%% You may obtain a copy of the License at
7%%
8%%      http://www.apache.org/licenses/LICENSE-2.0
9%%
10%% Unless required by applicable law or agreed to in writing, software
11%% distributed under the License is distributed on an "AS IS" BASIS,
12%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13%% See the License for the specific language governing permissions and
14%% limitations under the License.
15%%
16%% @doc helpers for validating REST API's parameters
17
18-module(validator).
19
20-include("cut.hrl").
21-include("pipes.hrl").
22-include("ns_common.hrl").
23
24
25-export([handle/4,
26         touch/2,
27         validate/3,
28         get_value/2,
29         convert/3,
30         one_of/3,
31         boolean/2,
32         integer/2,
33         integer/4,
34         range/4,
35         range/5,
36         dir/2,
37         has_params/1,
38         unsupported/1,
39         required/2,
40         prohibited/2,
41         return_value/3,
42         return_error/3]).
43
44-record(state, {kv = [], touched = [], errors = []}).
45
46handle(Fun, Req, json, Validators) ->
47    handle(Fun, Req, with_json_object(Req:recv_body(), Validators));
48
49handle(Fun, Req, form, Validators) ->
50    handle(Fun, Req, Req:parse_post(), Validators);
51
52handle(Fun, Req, qs, Validators) ->
53    handle(Fun, Req, Req:parse_qs(), Validators);
54
55handle(Fun, Req, Args, Validators) ->
56    handle(Fun, Req, functools:chain(#state{kv = Args}, Validators)).
57
58handle(Fun, Req, #state{kv = Props, errors = Errors, touched = Touched}) ->
59    ValidateOnly = proplists:get_value("just_validate", Req:parse_qs()) =:= "1",
60    case {ValidateOnly, Errors} of
61        {true, _} ->
62            menelaus_util:reply_json(
63              Req, {struct, [{errors, {struct, Errors}}]}, 200);
64        {false, []} ->
65            Props1 =
66                lists:map(fun ({K, V}) ->
67                                  case lists:member(K, Touched) of
68                                      true ->
69                                          {list_to_atom(K), V};
70                                      false ->
71                                          {K, V}
72                                  end
73                          end, Props),
74            Fun(Props1);
75        {false, _} ->
76            menelaus_util:reply_json(
77              Req, {struct, [{errors, {struct, Errors}}]}, 400)
78    end.
79
80with_json_object(Body, Validators) ->
81    try ejson:decode(Body) of
82        {KVList} ->
83            Params = [{binary_to_list(Name), Value} ||
84                         {Name, Value} <- KVList],
85            functools:chain(#state{kv = Params}, Validators);
86        _ ->
87            #state{errors = [{<<"_">>, <<"Unexpected Json">>}]}
88    catch _:_ ->
89            #state{errors = [{<<"_">>, <<"Invalid Json">>}]}
90    end.
91
92name_to_list(Name) when is_atom(Name) ->
93    atom_to_list(Name);
94name_to_list(Name) when is_list(Name) ->
95    Name.
96
97
98get_value(Name, #state{kv = Props, errors = Errors}) ->
99    LName = name_to_list(Name),
100    case proplists:get_value(LName, Props) of
101        undefined ->
102            undefined;
103        Value ->
104            case lists:keymember(LName, 1, Errors) of
105                true ->
106                    undefined;
107                false ->
108                    Value
109            end
110    end.
111
112touch(Name, #state{touched = Touched} = State) ->
113    LName = name_to_list(Name),
114    case lists:member(LName, Touched) of
115        true ->
116            State;
117        false ->
118            State#state{touched = [LName | Touched]}
119    end.
120
121return_value(Name, Value, #state{kv = Props} = State) ->
122    LName = name_to_list(Name),
123    State1 = touch(LName, State),
124    State1#state{kv = lists:keystore(LName, 1, Props, {LName, Value})}.
125
126return_error(Name, Error, #state{errors = Errors} = State) ->
127    State#state{errors = [{name_to_list(Name),
128                           iolist_to_binary(Error)} | Errors]}.
129
130validate(Fun, Name, State0) ->
131    State = touch(Name, State0),
132    case get_value(Name, State) of
133        undefined ->
134            State;
135        Value ->
136            case Fun(Value) of
137                ok ->
138                    State;
139                {value, V} ->
140                    return_value(Name, V, State);
141                {error, Error} ->
142                    return_error(Name, Error, State)
143            end
144    end.
145
146convert(Name, Fun, State) ->
147    validate(?cut({value, Fun(_)}), Name, State).
148
149simple_term_to_list(X) when is_atom(X) ->
150    atom_to_list(X);
151simple_term_to_list(X) when is_integer(X) ->
152    integer_to_list(X);
153simple_term_to_list(X) when is_binary(X) ->
154    binary_to_list(X);
155simple_term_to_list(X) when is_list(X) ->
156    X.
157
158simple_term_to_atom(X) when is_binary(X) ->
159    list_to_atom(binary_to_list(X));
160simple_term_to_atom(X) when is_list(X) ->
161    list_to_atom(X);
162simple_term_to_atom(X) when is_atom(X) ->
163    X.
164
165simple_term_to_integer(X) when is_list(X) ->
166    erlang:list_to_integer(X);
167simple_term_to_integer(X) when is_integer(X) ->
168    X.
169
170one_of(Name, List, State) ->
171    StringList = [simple_term_to_list(X) || X <- List],
172    validate(
173      fun (Value) ->
174              StringValue = (catch simple_term_to_list(Value)),
175              case lists:member(StringValue, StringList) of
176                  true ->
177                      ok;
178                  false ->
179                      {error,
180                       io_lib:format(
181                         "The value must be one of the following: [~s]",
182                         [string:join(StringList, ",")])}
183              end
184      end, Name, State).
185
186boolean(Name, State) ->
187    functools:chain(State,
188                    [one_of(Name, [true, false], _),
189                     convert(Name, fun simple_term_to_atom/1, _)]).
190
191integer(Name, State) ->
192    validate(
193      fun (Value) ->
194              Int = (catch simple_term_to_integer(Value)),
195              case is_integer(Int) of
196                  true ->
197                      {value, Int};
198                  false ->
199                      {error, "The value must be an integer"}
200              end
201      end, Name, State).
202
203integer(Name, Min, Max, State) ->
204    functools:chain(State,
205                    [integer(Name, _),
206                     range(Name, Min, Max, _)]).
207
208range(Name, Min, Max, State) ->
209    ErrorFun =
210        ?cut(io_lib:format("The value must be in range from ~p to ~p",
211                           [Min, Max])),
212    range(Name, Min, Max, ErrorFun, State).
213
214range(Name, Min, Max0, ErrorFun, State) ->
215    Max = case Max0 of
216              infinity ->
217                  1 bsl 64 - 1;
218              _ ->
219                  Max0
220          end,
221    validate(
222      fun (Value) ->
223              case Value >= Min andalso Value =< Max of
224                  true ->
225                      ok;
226                  false ->
227                      {error, ErrorFun()}
228              end
229      end, Name, State).
230
231dir(Name, State) ->
232    validate(fun (Value) ->
233                     case filelib:is_dir(Value) of
234                         true ->
235                             ok;
236                         false ->
237                             {error, "The value must be a valid directory"}
238                     end
239             end, Name, State).
240
241has_params(#state{kv = []} = State) ->
242    return_error("_", "Request should have parameters", State);
243has_params(State) ->
244    State.
245
246unsupported(#state{kv = Props, touched = Touched, errors = Errors} = State) ->
247    NewErrors =
248        lists:filtermap(
249          fun({Name, _}) ->
250                  case lists:member(Name, Touched) of
251                      true ->
252                          false;
253                      false ->
254                          {true, {Name, <<"Unsupported key">>}}
255                  end
256          end, Props),
257    State#state{errors = NewErrors ++ Errors}.
258
259required(Name, #state{kv = Props} = State) ->
260    functools:chain(
261      State,
262      [touch(Name, _),
263       fun (St) ->
264          case lists:keymember(name_to_list(Name), 1, Props) of
265              false ->
266                  return_error(Name, "The value must be supplied", St);
267              true ->
268                  St
269          end
270       end]).
271
272prohibited(Name, #state{kv = Props} = State) ->
273    case lists:keymember(name_to_list(Name), 1, Props) of
274        false ->
275            State;
276        true ->
277            return_error(Name, "The value must not be supplied", State)
278    end.
279