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