Validate shard specific query params on db create request
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 6a3df6d..5af6593 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -383,17 +383,10 @@
 
 create_db_req(#httpd{}=Req, DbName) ->
     couch_httpd:verify_is_server_admin(Req),
-    N = chttpd:qs_value(Req, "n", config:get("cluster", "n", "3")),
-    Q = chttpd:qs_value(Req, "q", config:get("cluster", "q", "8")),
-    P = chttpd:qs_value(Req, "placement", config:get("cluster", "placement")),
+    ShardsOpt = parse_shards_opt(Req),
     EngineOpt = parse_engine_opt(Req),
     DbProps = parse_partitioned_opt(Req),
-    Options = [
-        {n, N},
-        {q, Q},
-        {placement, P},
-        {props, DbProps}
-    ] ++ EngineOpt,
+    Options = lists:append([ShardsOpt, [{props, DbProps}], EngineOpt]),
     DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
     case fabric:create_db(DbName, Options) of
     ok ->
@@ -1702,6 +1695,40 @@
 parse_doc_query(Req) ->
     lists:foldl(fun parse_doc_query/2, #doc_query_args{}, chttpd:qs(Req)).
 
+parse_shards_opt(Req) ->
+    [
+        {n, parse_shards_opt("n", Req, config:get("cluster", "n", "3"))},
+        {q, parse_shards_opt("q", Req, config:get("cluster", "q", "8"))},
+        {placement, parse_shards_opt(
+            "placement", Req, config:get("cluster", "placement"))}
+    ].
+
+parse_shards_opt("placement", Req, Default) ->
+    Err = <<"The `placement` value should be in a format `zone:n`.">>,
+    case chttpd:qs_value(Req, "placement", Default) of
+        Default -> Default;
+        [] -> throw({bad_request, Err});
+        Val ->
+            try
+                true = lists:all(fun(Rule) ->
+                    [_, N] = string:tokens(Rule, ":"),
+                    couch_util:validate_positive_int(N)
+                end, string:tokens(Val, ",")),
+                Val
+            catch _:_ ->
+                throw({bad_request, Err})
+            end
+    end;
+
+parse_shards_opt(Param, Req, Default) ->
+    Val = chttpd:qs_value(Req, Param, Default),
+    Err = ?l2b(["The `", Param, "` value should be a positive integer."]),
+    case couch_util:validate_positive_int(Val) of
+        true -> Val;
+        false -> throw({bad_request, Err})
+    end.
+
+
 parse_engine_opt(Req) ->
     case chttpd:qs_value(Req, "engine") of
         undefined ->
@@ -2118,8 +2145,26 @@
         ]
     }.
 
+parse_shards_opt_test_() ->
+    {
+        foreach,
+        fun setup/0,
+        fun teardown/1,
+        [
+            t_should_allow_valid_q(),
+            t_should_default_on_missing_q(),
+            t_should_throw_on_invalid_q(),
+            t_should_allow_valid_n(),
+            t_should_default_on_missing_n(),
+            t_should_throw_on_invalid_n(),
+            t_should_allow_valid_placement(),
+            t_should_default_on_missing_placement(),
+            t_should_throw_on_invalid_placement()
+        ]
+    }.
 
 setup() ->
+    meck:expect(config, get, fun(_, _, Default) -> Default end),
     ok.
 
 teardown(_) ->
@@ -2158,4 +2203,103 @@
         ?assertEqual(parse_partitioned_opt(Req), [])
     end).
 
+t_should_allow_valid_q() ->
+    ?_test(begin
+        Req = mock_request("/all-test21?q=1"),
+        Opts = parse_shards_opt(Req),
+        ?assertEqual("1", couch_util:get_value(q, Opts))
+    end).
+
+t_should_default_on_missing_q() ->
+    ?_test(begin
+        Req = mock_request("/all-test21"),
+        Opts = parse_shards_opt(Req),
+        ?assertEqual("8", couch_util:get_value(q, Opts))
+    end).
+
+t_should_throw_on_invalid_q() ->
+    ?_test(begin
+        Req = mock_request("/all-test21?q="),
+        Err = <<"The `q` value should be a positive integer.">>,
+        ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+    end).
+
+t_should_allow_valid_n() ->
+    ?_test(begin
+        Req = mock_request("/all-test21?n=1"),
+        Opts = parse_shards_opt(Req),
+        ?assertEqual("1", couch_util:get_value(n, Opts))
+    end).
+
+t_should_default_on_missing_n() ->
+    ?_test(begin
+        Req = mock_request("/all-test21"),
+        Opts = parse_shards_opt(Req),
+        ?assertEqual("3", couch_util:get_value(n, Opts))
+    end).
+
+t_should_throw_on_invalid_n() ->
+    ?_test(begin
+        Req = mock_request("/all-test21?n="),
+        Err = <<"The `n` value should be a positive integer.">>,
+        ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+    end).
+
+t_should_allow_valid_placement() ->
+    {
+        foreach,
+        fun() -> ok end,
+        [
+            {"single zone",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement=az:1"),
+                Opts = parse_shards_opt(Req),
+                ?assertEqual("az:1", couch_util:get_value(placement, Opts))
+            end)},
+            {"multi zone",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement=az:1,co:3"),
+                Opts = parse_shards_opt(Req),
+                ?assertEqual("az:1,co:3",
+                    couch_util:get_value(placement, Opts))
+            end)}
+        ]
+    }.
+
+t_should_default_on_missing_placement() ->
+    ?_test(begin
+        Req = mock_request("/all-test21"),
+        Opts = parse_shards_opt(Req),
+        ?assertEqual(undefined, couch_util:get_value(placement, Opts))
+    end).
+
+t_should_throw_on_invalid_placement() ->
+    Err = <<"The `placement` value should be in a format `zone:n`.">>,
+    {
+        foreach,
+        fun() -> ok end,
+        [
+            {"empty placement",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement="),
+                ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+            end)},
+            {"invalid format",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement=moon"),
+                ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+            end)},
+            {"invalid n",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement=moon:eagle"),
+                ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+            end)},
+            {"one invalid zone",
+            ?_test(begin
+                Req = mock_request("/all-test21?placement=az:1,co:moon"),
+                ?assertThrow({bad_request, Err}, parse_shards_opt(Req))
+            end)}
+        ]
+    }.
+
 -endif.
diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl
index dffb681..95780e8 100644
--- a/src/couch/src/couch_util.erl
+++ b/src/couch/src/couch_util.erl
@@ -31,6 +31,7 @@
 -export([with_db/2]).
 -export([rfc1123_date/0, rfc1123_date/1]).
 -export([integer_to_boolean/1, boolean_to_integer/1]).
+-export([validate_positive_int/1]).
 -export([find_in_binary/2]).
 -export([callback_exists/3, validate_callback_exists/3]).
 -export([with_proc/4]).
@@ -624,6 +625,17 @@
     0.
 
 
+validate_positive_int(N) when is_list(N) ->
+    try
+        I = list_to_integer(N),
+        validate_positive_int(I)
+    catch error:badarg ->
+        false
+    end;
+validate_positive_int(N) when is_integer(N), N > 0 -> true;
+validate_positive_int(_) -> false.
+
+
 find_in_binary(_B, <<>>) ->
     not_found;