ninenines/cowboy — pull request #776 by manifest (2014-12-19 16:52:49+00:00)
Title: A bit more informative errors of constraints

Currently, we can't determine which of constraint fails and what a field for. in case the constraint returns `false`, we receive a not informative `{reason, case_clause}`. And when the constraint specified without a default value and a field isn't presented in the request, we receive `{reason,bad_key}`. We could make this behaviour a bit more informative by passing a field name and a constraint function with the exception `{bad_constraint, Key | {Key, Constraint}}`.

- `{bad_constraint, {Key, Constraint}}`
when the constraint returns 'false' and a field is presented in the request
- `{bad_constraint, Key}`
when the constraint specified without a default value and a field isn't presented in the request

```erlang
%% Example

byte_size_eq(Size) ->
fun
(Val) when byte_size(Val) =:= Size -> true;
(_) -> false
end.

malformed_request(Req, State) ->
ByteSizeEq1 = byte_size_eq(1),
Constraints =
[ {a, int, 42},
{b, ByteSizeEq1} ],

ErrorDesc =
fun
({Key, F}) ->
Field = atom_to_binary(Key, utf8),
case F of
int -> <<"The '", Field/binary, "' field must be an integer.">>;
ByteSizeEq1 -> <<"The '", Field/binary, "' field must be a bynary string of length 1.">>;
_ -> <<"Invalid value of the '", Field/binary, "' field.">>
end;
(Key) ->
<<"The '", (atom_to_binary(Key, utf8))/binary, "' field is required.">>
end,

try cowboy_req:match_qs(Constraints, Req) of
M ->
{false, Req, M}
catch
_:{bad_constraint, Reason} ->
error_logger:error_report([bad_constraint, {reason, Reason}]),
Req2 = cowboy_req:set_resp_body(jsxn:encode(#{error => ErrorDesc(Reason)}), Req),
Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2),
{true, Req3, State}
end.
```
```sh
## Case 1

curl -4i -XGET "http://localhost:8081/?a=A"
HTTP/1.1 400 Bad Request
connection: keep-alive
server: Cowboy
date: Fri, 19 Dec 2014 13:59:39 GMT
content-length: 45
content-type: application/json

{"error":"The 'a' field must be an integer."}
```
```erlang
=ERROR REPORT==== 19-Dec-2014::16:59:39 ===
bad_constraint
reason: {a,int}
```
```sh
## Case 2

curl -4i -XGET "http://localhost:8081/?a=1"
HTTP/1.1 400 Bad Request
connection: keep-alive
server: Cowboy
date: Fri, 19 Dec 2014 14:00:17 GMT
content-length: 32
content-type: application/json

{"error":"The 'b' field is required."}
```

```erlang
=ERROR REPORT==== 19-Dec-2014::17:00:18 ===
bad_constraint
reason: b
```
```sh
## Case 3

curl -4i -XGET "http://localhost:8081/?a=1&b=BB"
HTTP/1.1 400 Bad Request
connection: keep-alive
server: Cowboy
date: Fri, 19 Dec 2014 14:00:50 GMT
content-length: 62
content-type: application/json

{"error":"The 'b' field must be a bynary string of length 1."
```

```erlang
=ERROR REPORT==== 19-Dec-2014::17:00:50 ===
bad_constraint
reason: {b,#Fun}
```

```sh
## Case 4

curl -4i -XGET "http://localhost:8081/?a=1&b=B"
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Fri, 19 Dec 2014 14:01:16 GMT
content-length: 15
content-type: application/json

{"a":1,"b":"B"}
```