From 37c35677e07c4e6e0bd324fe8f37ddea09c83086 Mon Sep 17 00:00:00 2001
From: mechanik20051988 <mechanik20051988@tarantool.org>
Date: Tue, 16 Nov 2021 20:03:47 +0300
Subject: [PATCH] uri: implement ability to parse URIs passed in different ways

Previously, URI can be passed as a string, which contains one URI
or several URIs separated by commas. Now URIs can be passed in
different ways: as before, as a table which contains URI and it's
parameters in "param" table, as a table which contains URI strings
and URI tables. Also there are different ways to specify properties
for URI: in a string which contains URI, after '?' delimiter, in a
table which contains URI in "params" table, in "default_params" table
if it is default parameters for all URIs.
For this purposes new method `parse_many` was implemented in tarantool
`uri` library. Also `parse` method was updated to make possible the
same as new `parse_many` method but only for single URI.
```lua
uri = require('uri')
-- Single URI, passed as before
uri.parse_many("/tmp/unix.sock")
-- Single URI, with query paramters
uri.parse_many("/tmp/unix.sock?q1=v1&q2=v2")
-- Several URIs with parameters in one string, separated by commas
uri.parse_many("/tmp/unix.sock_1?q=v, /tmp/unix.sock_2?q=v")
-- Single URI passed in table, with additional parameters, passed
-- in "params" table. This parameters overwrite parameters from
-- URI string (q1 = "v2" in example below).
uri.parse_many({"/tmp/unix.sock?q1=v1", params = {q1 = "v2"}})
-- For parse it's also works now
uri.parse({"/tmp/unix.sock?q1=v1", params = {q1 = "v2"}})
-- Several URIs passed in table with default parameters, passed
-- in "default_params" table, which are used for parameters, which
-- not specified for URI (q3 parameter with "v3" value corresponds
-- to all URIs, and used if there is no such parameter in URI).
uri.parse_many({
    "/tmp/unix.sock_1?q1=v1",
    { uri = "/tmp/unix.sock_2", params = { q2 = "v2" } },
    default_params = { q3 = "v3" }
})
```
---
 extra/exports                                 |   2 -
 src/CMakeLists.txt                            |   1 +
 src/lib/uri/uri.c                             |  55 +-
 src/lib/uri/uri.h                             |  12 +
 src/lua/init.c                                |   2 +
 src/lua/uri.c                                 | 347 +++++++++++
 src/lua/uri.h                                 |  22 +
 src/lua/uri.lua                               |  41 +-
 .../test/static-build/exports.test.lua        |   2 -
 test/app-tap/uri.test.lua                     | 549 +++++++++++++++++-
 10 files changed, 998 insertions(+), 35 deletions(-)
 create mode 100644 src/lua/uri.c
 create mode 100644 src/lua/uri.h

diff --git a/extra/exports b/extra/exports
index 8fa79fabd4..dbb243f2ff 100644
--- a/extra/exports
+++ b/extra/exports
@@ -463,10 +463,8 @@ tt_uuid_from_string
 tt_uuid_is_equal
 tt_uuid_is_nil
 tt_uuid_to_string
-uri_create
 uri_destroy
 uri_format
-uri_set_create
 uri_set_destroy
 uuid_nil
 uuid_unpack
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3e385e7b9a..f9415290ea 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -148,6 +148,7 @@ set (server_sources
      lua/string.c
      lua/swim.c
      lua/decimal.c
+     lua/uri.c
      ${lua_sources}
      ${PROJECT_SOURCE_DIR}/third_party/lua-yaml/lyaml.cc
      ${PROJECT_SOURCE_DIR}/third_party/lua-yaml/b64.c
diff --git a/src/lib/uri/uri.c b/src/lib/uri/uri.c
index fa3cfdf755..e4ad60935d 100644
--- a/src/lib/uri/uri.c
+++ b/src/lib/uri/uri.c
@@ -66,11 +66,20 @@ uri_param_create(struct uri_param *param, const char *name)
 	param->value_count = 0;
 }
 
-/**
- * Appends @a value to @a uri parameter with given @a name,
- * creating one if it doesn't exist.
- */
-static void
+void
+uri_remove_param(struct uri *uri, const char *name)
+{
+	struct uri_param *param = uri_find_param(uri, name);
+	if (param == NULL)
+		return;
+	int idx = param - uri->params;
+	uri_param_destroy(param);
+	for (int i = idx; i < uri->param_count - 1; i++)
+		uri->params[i] = uri->params[i + 1];
+	uri->param_count--;
+}
+
+void
 uri_add_param(struct uri *uri, const char *name, const char *value)
 {
 	struct uri_param *param = uri_find_param(uri, name);
@@ -170,6 +179,38 @@ uri_create(struct uri *uri, const char *str)
 	return 0;
 }
 
+static int
+uri_format_param(char *str, int len, const struct uri_param *param)
+{
+	int total = 0;
+	if (param->value_count == 0) {
+		SNPRINT(total, snprintf, str, len, "%s", param->name);
+		return total;
+	}
+	for (int i = 0; i < param->value_count; i++) {
+		SNPRINT(total, snprintf, str, len, "%s=%s",
+			param->name, param->values[i]);
+		if (i != param->value_count - 1)
+			SNPRINT(total, snprintf, str, len, "&");
+	}
+	return total;
+}
+
+static int
+uri_format_params(char *str, int len, const struct uri *uri)
+{
+	if (uri->param_count == 0)
+		return 0;
+	int total = 0;
+	SNPRINT(total, snprintf, str, len, "?");
+	for (int i = 0; i < uri->param_count; i++) {
+		SNPRINT(total, uri_format_param, str, len, &uri->params[i]);
+		if (i != uri->param_count - 1)
+			SNPRINT(total, snprintf, str, len, "&");
+	}
+	return total;
+}
+
 int
 uri_format(char *str, int len, const struct uri *uri, bool write_password)
 {
@@ -193,8 +234,8 @@ uri_format(char *str, int len, const struct uri *uri, bool write_password)
 	if (uri->path != NULL) {
 		SNPRINT(total, snprintf, str, len, "%s", uri->path);
 	}
-	if (uri->query != NULL) {
-		SNPRINT(total, snprintf, str, len, "?%s", uri->query);
+	if (uri->params != NULL) {
+		SNPRINT(total, uri_format_params, str, len, uri);
 	}
 	if (uri->fragment != NULL) {
 		SNPRINT(total, snprintf, str, len, "#%s", uri->fragment);
diff --git a/src/lib/uri/uri.h b/src/lib/uri/uri.h
index c568d84b9f..bdbc458e58 100644
--- a/src/lib/uri/uri.h
+++ b/src/lib/uri/uri.h
@@ -42,6 +42,18 @@ struct uri_set {
 #define URI_MAXHOST NI_MAXHOST
 #define URI_MAXSERVICE _POSIX_PATH_MAX /* _POSIX_PATH_MAX always > NI_MAXSERV */
 
+/**
+ * Appends @a value to @a uri parameter with given @a name,
+ * creating one if it doesn't exist.
+ */
+void
+uri_add_param(struct uri *uri, const char *name, const char *value);
+
+/**
+ * Remove @a uri parameter and all its values.
+ */
+void
+uri_remove_param(struct uri *uri, const char *name);
 
 /**
  * Creates new @a uri structure according to passed @a str.
diff --git a/src/lua/init.c b/src/lua/init.c
index dbb1a0175e..a662ad0500 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -62,6 +62,7 @@
 #include "lua/utf8.h"
 #include "lua/swim.h"
 #include "lua/decimal.h"
+#include "lua/uri.h"
 #include "digest.h"
 #include "errinj.h"
 #include <small/ibuf.h>
@@ -669,6 +670,7 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
 	lua_call(L, 0, 0);
 	lua_register(L, "tonumber64", lbox_tonumber64);
 
+	tarantool_lua_uri_init(L);
 	tarantool_lua_utf8_init(L);
 	tarantool_lua_utils_init(L);
 	tarantool_lua_fiber_init(L);
diff --git a/src/lua/uri.c b/src/lua/uri.c
new file mode 100644
index 0000000000..156b847c3a
--- /dev/null
+++ b/src/lua/uri.c
@@ -0,0 +1,347 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "lua/uri.h"
+
+#include "lua/utils.h"
+#include "uri/uri.h"
+#include "diag.h"
+
+/**
+ * Add or overwrite (depends on @a overwrite) URI parameter to @a uri.
+ * Parameter value is located at the top of the lua stack, parameter name is
+ * in the position next to it. Allowed types for URI parameter values
+ * are LUA_TSTRING, LUA_TNUMBER and LUA_TTABLE. URI parameter name should
+ * be a string.
+ */
+static int
+uri_add_param_from_lua(struct uri *uri, struct lua_State *L, bool overwrite)
+{
+	if (lua_type(L, -2) != LUA_TSTRING) {
+		diag_set(IllegalParams, "Incorrect type for URI "
+			 "parameter name: should be a string");
+		return -1;
+	}
+	const char *name = lua_tostring(L, -2);
+	if (overwrite) {
+		uri_remove_param(uri, name);
+	} else if (uri_param_count(uri, name) != 0) {
+		return 0;
+	}
+	int rc = 0;
+	switch (lua_type(L, -1)) {
+	case LUA_TSTRING:
+	case LUA_TNUMBER:
+		uri_add_param(uri, name, lua_tostring(L, -1));
+		break;
+	case LUA_TTABLE:
+		for (unsigned i = 0; i < lua_objlen(L, -1) && rc == 0; i++) {
+			lua_rawgeti(L, -1, i + 1);
+			const char *value = lua_tostring(L, -1);
+			if (value != NULL) {
+				uri_add_param(uri, name, value);
+			} else {
+				diag_set(IllegalParams, "Incorrect type for "
+					 "URI parameter value: should "
+					 "be string or number");
+				rc = -1;
+			}
+			lua_pop(L, 1);
+		}
+		break;
+	default:
+		diag_set(IllegalParams, "Incorrect type for URI "
+			 "parameter value: should be string, number or table");
+		rc = -1;
+	}
+	return rc;
+}
+
+/**
+ * Add or overwrite (depends on @a overwrite) URI parameters in @a uri.
+ * Table with parameters or nil value should be located at the top of the lua
+ * stack.
+ */
+static int
+uri_add_params_from_lua(struct uri *uri, struct lua_State *L, bool overwrite)
+{
+	if (lua_type(L, -1) == LUA_TNIL) {
+		return 0;
+	} else if (lua_type(L, -1) != LUA_TTABLE) {
+		diag_set(IllegalParams, "Incorrect type for URI "
+			 "parameters: should be a table");
+		return -1;
+	}
+	int rc = 0;
+	lua_pushnil(L);
+	while (lua_next(L, -2) != 0 && rc == 0) {
+		rc = uri_add_param_from_lua(uri, L, overwrite);
+		assert(rc == 0 || !diag_is_empty(diag_get()));
+		lua_pop(L, 1);
+	}
+	return rc;
+}
+
+/**
+ * Returns the type of the field, which located at the given valid
+ * @a index in the table which located at the given valid @a table_idx.
+ */
+static int
+field_type(struct lua_State *L, int table_idx, int index)
+{
+	assert(lua_type(L, table_idx) == LUA_TTABLE);
+	lua_rawgeti(L, table_idx, index);
+	int rc = lua_type(L, -1);
+	lua_pop(L, 1);
+	return rc;
+}
+
+/**
+ * Check if there is a field with the name @a name in the table,
+ * which located at the given valid @a idx, which should be a
+ * positive value.
+ */
+static bool
+is_field_present(struct lua_State *L, int idx, const char *name)
+{
+	assert(idx > 0);
+	assert(lua_type(L, idx) == LUA_TTABLE);
+	lua_pushstring(L, name);
+	lua_rawget(L, idx);
+	bool field_is_present = (lua_type(L, -1) != LUA_TNIL);
+	lua_pop(L, 1);
+	return field_is_present;
+}
+
+/**
+ * Create @a uri from the table, which located at the given valid @a idx,
+ * which should be a positive value.
+ */
+static int
+uri_create_from_lua_table(struct lua_State *L, int idx, struct uri *uri)
+{
+	assert(idx > 0);
+	assert(lua_type(L, idx) == LUA_TTABLE);
+	/* There should be exactly one URI in the table */
+	int size = lua_objlen(L, idx);
+	int uri_count = size + is_field_present(L, idx, "uri");
+	if (uri_count != 1) {
+		diag_set(IllegalParams, "Invalid URI table: "
+			 "expected {uri = string, params = table} "
+			 "or {string, params = table}");
+		return -1;
+	}
+	/* Table "default_params" is not allowed for single URI */
+	if (is_field_present(L, idx, "default_params")) {
+		diag_set(IllegalParams, "Default URI parameters are "
+			 "not allowed for single URI");
+		return -1;
+	}
+	int rc = 0;
+	if (size == 1) {
+		lua_rawgeti(L, idx, 1);
+	} else {
+		lua_pushstring(L, "uri");
+		lua_rawget(L, idx);
+	}
+	const char *uristr = lua_tostring(L, -1);
+	if (uristr != NULL) {
+		rc = uri_create(uri, uristr);
+		if (rc != 0) {
+			diag_set(IllegalParams, "Incorrect URI: expected "
+				 "host:service or /unix.socket");
+		}
+	} else {
+		diag_set(IllegalParams, "Incorrect type for URI in nested "
+			 "table: should be string, number");
+		rc = -1;
+	}
+	lua_pop(L, 1);
+	if (rc != 0)
+		return rc;
+	lua_pushstring(L, "params");
+	lua_rawget(L, idx);
+	rc = uri_add_params_from_lua(uri, L, true);
+	lua_pop(L, 1);
+	if (rc != 0)
+		uri_destroy(uri);
+	return rc;
+
+}
+
+/**
+ * Create @a uri from the value at the given valid @a idx.
+ */
+static int
+luaT_uri_create(struct lua_State *L, int idx, struct uri *uri)
+{
+	int rc = 0;
+	if (idx < 0)
+		idx = lua_gettop(L) + idx + 1;
+	assert(idx > 0);
+	if (lua_isstring(L, idx)) {
+		rc = uri_create(uri, lua_tostring(L, idx));
+		if (rc != 0) {
+			diag_set(IllegalParams, "Incorrect URI: "
+				 "expected host:service or "
+				 "/unix.socket");
+		}
+	} else if (lua_istable(L, idx)) {
+		rc = uri_create_from_lua_table(L, idx, uri);
+	} else if (lua_isnil(L, idx)) {
+		uri_create(uri, NULL);
+	} else {
+		diag_set(IllegalParams, "Incorrect type for URI: "
+			 "should be string, number or table");
+		rc = -1;
+	}
+	assert(rc == 0 || !diag_is_empty(diag_get()));
+	return rc;
+}
+
+/**
+ * Create @a uri_set from the table, which located at the given valid @a idx,
+ * which should be a positive value.
+ */
+static int
+uri_set_create_from_lua_table(struct lua_State *L, int idx,
+			      struct uri_set *uri_set)
+{
+	int rc = 0;
+	assert(idx > 0);
+	assert(lua_type(L, idx) == LUA_TTABLE);
+	int size = lua_objlen(L, idx);
+	struct uri uri;
+
+	uri_set_create(uri_set, NULL);
+	if (is_field_present(L, idx, "uri") ||
+	    (size == 1 && field_type(L, idx, 1) != LUA_TTABLE)) {
+		rc = luaT_uri_create(L, idx, &uri);
+		if (rc == 0) {
+			uri_set_add(uri_set, &uri);
+			uri_destroy(&uri);
+		}
+		return rc;
+	} else if (size == 0) {
+		return 0;
+	}
+
+	/*
+	 * All numeric keys corresponds to URIs in string or table
+	 * format.
+	 */
+	for (int i = 0; i < size && rc == 0; i++) {
+		lua_rawgeti(L, idx, i + 1);
+		rc = luaT_uri_create(L, -1, &uri);
+		if (rc == 0) {
+			uri_set_add(uri_set, &uri);
+			uri_destroy(&uri);
+		}
+		lua_pop(L, 1);
+	}
+	if (rc != 0)
+		goto fail;
+
+	/*
+	 * Here we are only in case when it is an URI array, so it
+	 * shouldn't be "params" field here.
+	 */
+	if (is_field_present(L, idx, "params")) {
+		diag_set(IllegalParams, "URI parameters are "
+			 "not allowed for multiple URIs");
+		goto fail;
+	}
+
+	lua_pushstring(L, "default_params");
+	lua_rawget(L, idx);
+	if (!lua_isnil(L, -1)) {
+		for (int i = 0; i < uri_set->uri_count && rc == 0; i++) {
+			struct uri *uri = &uri_set->uris[i];
+			rc = uri_add_params_from_lua(uri, L, false);
+			assert(rc == 0 || !diag_is_empty(diag_get()));
+		}
+	}
+	lua_pop(L, 1);
+	if (rc != 0)
+		goto fail;
+	return 0;
+fail:
+	uri_set_destroy(uri_set);
+	return -1;
+}
+
+/**
+ * Create @a uri_set from the value at the given valid @a idx.
+ */
+static int
+luaT_uri_set_create(struct lua_State *L, int idx, struct uri_set *uri_set)
+{
+	int rc = 0;
+	if (idx < 0)
+		idx = lua_gettop(L) + idx + 1;
+	assert(idx > 0);
+	if (lua_isstring(L, idx)) {
+		rc = uri_set_create(uri_set, lua_tostring(L, idx));
+		if (rc != 0) {
+			diag_set(IllegalParams, "Incorrect URI: "
+				 "expected host:service or "
+				 "/unix.socket");
+		}
+	} else if (lua_istable(L, idx)) {
+		rc = uri_set_create_from_lua_table(L, idx, uri_set);
+	} else if (lua_isnil(L, idx)) {
+		uri_set_create(uri_set, NULL);
+	} else {
+		diag_set(IllegalParams, "Incorrect type for URI: "
+			 "should be string, number or table");
+		rc = -1;
+	}
+	assert(rc == 0 || !diag_is_empty(diag_get()));
+	return rc;
+}
+
+static int
+luaT_uri_create_internal(lua_State *L)
+{
+	struct uri *uri = (struct uri *)lua_topointer(L, 1);
+	if (uri == NULL)
+		luaL_error(L, "Usage: uri.internal.uri_create(string|table)");
+	if (luaT_uri_create(L, 2, uri) != 0)
+		luaT_error(L);
+	return 0;
+}
+
+static int
+luaT_uri_set_create_internal(lua_State *L)
+{
+	struct uri_set *uri_set = (struct uri_set *)lua_topointer(L, 1);
+	if (uri_set == NULL)
+		luaL_error(L, "Usage: uri.internal.uri_set_create(string|table)");
+	if (luaT_uri_set_create(L, 2, uri_set) != 0)
+		luaT_error(L);
+	return 0;
+}
+
+void
+tarantool_lua_uri_init(struct lua_State *L)
+{
+	static const struct luaL_Reg uri_methods[] = {
+		{NULL, NULL}
+	};
+	luaL_register_module(L, "uri", uri_methods);
+
+	/* internal table */
+	lua_pushliteral(L, "internal");
+	lua_newtable(L);
+	static const struct luaL_Reg uri_internal_methods[] = {
+		{"uri_create", luaT_uri_create_internal},
+		{"uri_set_create", luaT_uri_set_create_internal},
+		{NULL, NULL}
+	};
+	luaL_register(L, NULL, uri_internal_methods);
+	lua_settable(L, -3);
+
+	lua_pop(L, 1);
+};
diff --git a/src/lua/uri.h b/src/lua/uri.h
new file mode 100644
index 0000000000..307e4de287
--- /dev/null
+++ b/src/lua/uri.h
@@ -0,0 +1,22 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#pragma once
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct lua_State;
+
+/**
+* Initialize box.uri system
+*/
+void
+tarantool_lua_uri_init(struct lua_State *L);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/lua/uri.lua b/src/lua/uri.lua
index 98d5aa7cee..f2f7365bfe 100644
--- a/src/lua/uri.lua
+++ b/src/lua/uri.lua
@@ -2,6 +2,7 @@
 
 local ffi = require('ffi')
 local buffer = require('buffer')
+local uri = require('uri')
 
 ffi.cdef[[
 struct uri_param {
@@ -36,15 +37,9 @@ struct uri_set {
     struct uri *uris;
 };
 
-int
-uri_create(struct uri *uri, const char *str);
-
 void
 uri_destroy(struct uri *uri);
 
-int
-uri_set_create(struct uri_set *uri_set, const char *str);
-
 void
 uri_set_destroy(struct uri_set *uri_set);
 
@@ -93,12 +88,13 @@ end
 
 local function parse(str)
     if str == nil then
-        error("Usage: uri.parse(string)")
+        error("Usage: uri.parse(string|table)")
     end
     local uribuf = uri_stash_take()
-    if builtin.uri_create(uribuf, str) ~= 0 then
+    local status, errmsg = pcall(uri.internal.uri_create, uribuf, str)
+    if not status then
         uri_stash_put(uribuf)
-        return nil
+        return nil, errmsg
     end
     local result = parse_uribuf(uribuf)
     builtin.uri_destroy(uribuf)
@@ -108,12 +104,13 @@ end
 
 local function parse_many(str)
     if str == nil then
-        error("Usage: uri.parse_many(string)")
+        error("Usage: uri.parse_many(string|table)")
     end
     local uri_set_buf = uri_set_stash_take()
-    if builtin.uri_set_create(uri_set_buf, str) ~= 0 then
+    local status, errmsg = pcall(uri.internal.uri_set_create, uri_set_buf, str)
+    if not status then
         uri_set_stash_put(uri_set_buf)
-        return nil
+        return nil, errmsg
     end
     local result = {}
     for i = 0, uri_set_buf.uri_count - 1 do
@@ -124,6 +121,25 @@ local function parse_many(str)
     return result
 end
 
+local function fill_uribuf_params(uribuf, uri)
+    uribuf.param_count = 0
+    for _, _ in pairs(uri.params) do
+        uribuf.param_count = uribuf.param_count + 1
+    end
+    uribuf.params = ffi.new("struct uri_param[?]", uribuf.param_count)
+    local i = 0
+    for param_name, param in pairs(uri.params) do
+        uribuf.params[i].value_count = #param
+        uribuf.params[i].name = param_name
+        uribuf.params[i].values =
+            ffi.new("const char *[?]", uribuf.params[i].value_count)
+        for j = 1, uribuf.params[i].value_count do
+            uribuf.params[i].values[j - 1] = param[j]
+        end
+        i = i + 1
+    end
+end
+
 local function format(uri, write_password)
     local uribuf = uri_stash_take()
     uribuf.scheme = uri.scheme
@@ -134,6 +150,7 @@ local function format(uri, write_password)
     uribuf.path = uri.path
     uribuf.query = uri.query
     uribuf.fragment = uri.fragment
+    fill_uribuf_params(uribuf, uri)
     local ibuf = cord_ibuf_take()
     local str = ibuf:alloc(1024)
     local len = builtin.uri_format(str, 1024, uribuf, write_password and 1 or 0)
diff --git a/static-build/test/static-build/exports.test.lua b/static-build/test/static-build/exports.test.lua
index f0a0870e4a..2b23062cf3 100755
--- a/static-build/test/static-build/exports.test.lua
+++ b/static-build/test/static-build/exports.test.lua
@@ -63,10 +63,8 @@ local check_symbols = {
     'tt_uuid_to_string',
     'log_level',
     'log_format',
-    'uri_create',
     'uri_destroy',
     'uri_format',
-    'uri_set_create',
     'uri_set_destroy',
     'PMurHash32',
     'PMurHash32_Process',
diff --git a/test/app-tap/uri.test.lua b/test/app-tap/uri.test.lua
index 9830587e9d..9bc480511b 100755
--- a/test/app-tap/uri.test.lua
+++ b/test/app-tap/uri.test.lua
@@ -6,7 +6,7 @@ local uri = require('uri')
 local function test_parse(test)
     -- Tests for uri.parse() Lua bindings.
     -- Parser itself is tested by test/unit/uri_parser unit test.
-    test:plan(54)
+    test:plan(56)
 
     local u
 
@@ -85,24 +85,77 @@ local function test_parse(test)
     test:is(u.unix, '/tmp/unix.sock', 'unix')
     test:is(u.path, '/path1/path2/path3', 'path')
 
-    u = uri.parse("")
+    local error, expected_errmsg
+
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    u, error = uri.parse("")
     test:isnil(u, "invalid uri", u)
-    u = uri.parse("://")
+    test:is(tostring(error), expected_errmsg, "error message")
+    u, error = uri.parse("://")
     test:isnil(u, "invalid uri", u)
+    test:is(tostring(error), expected_errmsg, "error message")
 end
 
 local function test_format(test)
-    test:plan(3)
+    test:plan(12)
     local u = uri.parse("user:password@localhost")
     test:is(uri.format(u), "user@localhost", "password removed")
     test:is(uri.format(u, false), "user@localhost", "password removed")
     test:is(uri.format(u, true), "user:password@localhost", "password kept")
+
+    -- URI with empty query
+     u = uri.parse({"/tmp/unix.sock?"})
+     test:is(uri.format(u), "unix/:/tmp/unix.sock", "URI format")
+
+     -- URI with one empty parameter
+    u = uri.parse({"/tmp/unix.sock?q1"})
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1", "URI format")
+
+    -- All parameters passed in "params" table.
+    u = uri.parse({"/tmp/unix.sock", params = {q1 = "v1", q2 = "v2"}})
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=v1&q2=v2", "URI format")
+
+    -- Empty parameter in URI string.
+    u = uri.parse({"/tmp/unix.sock?q1", params = {q2 = "v2", q3 = "v3"}})
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1&q2=v2&q3=v3", "URI format")
+
+    -- Parameter without value in URI string.
+    u = uri.parse({"/tmp/unix.sock?q1=", params = {q2 = "v2", q3 = "v3"}})
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=&q2=v2&q3=v3", "URI format")
+
+    -- Some parameters passed in URI string and some different
+    -- parameters passed in "params" table.
+    u = uri.parse({"/tmp/unix.sock?q1=v1", params = {q2 = "v2"}})
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=v1&q2=v2", "URI format")
+
+    -- Same as previous but each parameter has several values.
+    u = uri.parse({
+        "/tmp/unix.sock?q1=v11&q1=v12",
+        params = {q2 = {"v21", "v22"}}
+    })
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=v11&q1=v12&q2=v21&q2=v22",
+            "URI format")
+
+    -- One of parameters in "param" table has empty value
+    u = uri.parse({
+        "/tmp/unix.sock?q1=v11&q1=v12",
+        params = {q2 = {""}}
+    })
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=v11&q1=v12&q2=", "URI format")
+
+    -- Parameter from "params" table overwrite
+    -- parameter from URI string.
+    u = uri.parse({
+        "/tmp/unix.sock?q1=v11&q1=v12",
+        params = {q1 = {"v13", "v14"}, q2 = "v2"}
+    })
+    test:is(uri.format(u), "unix/:/tmp/unix.sock?q1=v13&q1=v14&q2=v2", "URI format")
 end
 
 local function test_parse_uri_query_params(test)
     -- Tests for uri.parse() Lua bindings (URI with query parameters).
     -- Parser itself is tested by test/unit/uri unit test.
-    test:plan(33)
+    test:plan(55)
 
     local u
 
@@ -146,12 +199,57 @@ local function test_parse_uri_query_params(test)
     test:is(u.params["q1"][2], "", "param value")
     test:is(type(u.params["q2"]), "table", "name")
     test:is(#u.params["q2"], 0, "value count")
+
+    -- Parse URI passed in table format, parameter values
+    -- from "params" table, overwrite values from string.
+    u = uri.parse({
+        "/tmp/unix.sock?q1=v11",
+        params = { q1 = "v12", q2 = "v21" }
+    })
+    test:is(u.host, 'unix/', 'host')
+    test:is(u.service, '/tmp/unix.sock', 'service')
+    test:is(u.unix, '/tmp/unix.sock', 'unix')
+    test:is(type(u.params["q1"]), "table", "name")
+    test:is(#u.params["q1"], 1, "value count")
+    test:is(u.params["q1"][1], "v12", "param value")
+    test:is(type(u.params["q2"]), "table", "name")
+    test:is(#u.params["q2"], 1, "value count")
+    test:is(u.params["q2"][1], "v21", "param value")
+
+    -- Same as previous but "uri=" syntax
+    u = uri.parse({
+        uri = "/tmp/unix.sock?q1=v11",
+        params = { q1 = "v12", q2 = "v21" }
+    })
+    test:is(u.host, 'unix/', 'host')
+    test:is(u.service, '/tmp/unix.sock', 'service')
+    test:is(u.unix, '/tmp/unix.sock', 'unix')
+    test:is(type(u.params["q1"]), "table", "name")
+    test:is(#u.params["q1"], 1, "value count")
+    test:is(u.params["q1"][1], "v12", "param value")
+    test:is(type(u.params["q2"]), "table", "name")
+    test:is(#u.params["q2"], 1, "value count")
+    test:is(u.params["q2"][1], "v21", "param value")
+
+    local error, expected_errmsg
+
+    -- "defult_params" is not allowed for single URI.
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    u, error = uri.parse({ "/tmp/unix.sock", default_params = {q = "v"} })
+    test:isnil(u, "invalid uri", u)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Multiple URIs is not allowed in `parse` method,
+    -- use `parse_many` instead.
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    u, error = uri.parse({ "/tmp/unix.sock, /tmp/unix.sock"})
+    test:isnil(u, "invalid uri", u)
+    test:is(tostring(error), expected_errmsg, "error message")
 end
 
 local function test_parse_uri_set_with_query_params(test)
     -- Tests for uri.parse_many() Lua bindings (several URIs with query parameters).
     -- Parser itself is tested by test/unit/uri unit test.
-    test:plan(52)
+    test:plan(57)
 
     local uri_set
 
@@ -198,31 +296,458 @@ local function test_parse_uri_set_with_query_params(test)
     uri_set = uri.parse_many("")
     test:is(#uri_set, 0, "uri_set")
 
+    local error, expected_errmsg
+
     -- Check that uri.parse_many return nil for invalid URIs string
     uri_set = uri.parse_many("tmp/unix.sock, ://")
     test:isnil(uri_set, "invalid uri", uri_set)
 
     -- Extra ',' is not allowed
-    uri_set = uri.parse_many("/tmp/unix.sock,,/tmp/unix.sock")
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    uri_set , error= uri.parse_many("/tmp/unix.sock,,/tmp/unix.sock")
     test:isnil(uri_set, "invalid uri", uri_set)
-    uri_set = uri.parse_many("/tmp/unix.sock, ,/tmp/unix.sock")
+    test:is(tostring(error), expected_errmsg, "error message")
+    uri_set, error = uri.parse_many("/tmp/unix.sock, ,/tmp/unix.sock")
     test:isnil(uri_set, "invalid uri", uri_set)
-    uri_set = uri.parse_many("/tmp/unix.sock,, /tmp/unix.sock")
+    test:is(tostring(error), expected_errmsg, "error message")
+    uri_set, error = uri.parse_many("/tmp/unix.sock,, /tmp/unix.sock")
     test:isnil(uri_set, "invalid uri", uri_set)
-    uri_set = uri.parse_many("/tmp/unix.sock ,,/tmp/unix.sock")
+    test:is(tostring(error), expected_errmsg, "error message")
+    uri_set, error = uri.parse_many("/tmp/unix.sock ,,/tmp/unix.sock")
     test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
 
 
     -- Check that we can't parse string with multiple URIs,
     -- using old method.
-    local u = uri.parse("/tmp/unix.sock, /tmp/unix.sock")
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    local u, error = uri.parse("/tmp/unix.sock, /tmp/unix.sock")
     test:isnil(u, "invalid uri", u)
+    test:is(tostring(error), expected_errmsg, "error message")
+end
+
+local function test_parse_uri_set_from_lua_table(test)
+    -- Tests for uri.parse_many() Lua bindings.
+    -- (Several URIs with parameters, passed in different ways).
+    test:plan(134)
+
+    local uri_set
+
+    -- Array with one string address and one parameter
+    uri_set = uri.parse_many({"/tmp/unix.sock?q1=v1"})
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "table", "name")
+    test:is(#uri_set[1].params["q1"], 1, "value count")
+    test:is(uri_set[1].params["q1"][1], "v1", "param value")
+
+    -- Array with one string address and several parameters.
+    -- One of them passed in URI string, one separately as a string,
+    -- one separately as a table with two values.
+    uri_set = uri.parse_many({
+        "/tmp/unix.sock?q1=v1",
+        params = {q2 = "v2", q3 = {"v31", "v32"}}
+    })
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "table", "name")
+    test:is(#uri_set[1].params["q1"], 1, "value count")
+    test:is(uri_set[1].params["q1"][1], "v1", "param value")
+    test:is(type(uri_set[1].params["q2"]), "table", "name")
+    test:is(#uri_set[1].params["q2"], 1, "value count")
+    test:is(uri_set[1].params["q2"][1], "v2", "param value")
+    test:is(type(uri_set[1].params["q3"]), "table", "name")
+    test:is(#uri_set[1].params["q3"], 2, "value count")
+    test:is(uri_set[1].params["q3"][1], "v31", "param value")
+    test:is(uri_set[1].params["q3"][2], "v32", "param value")
+
+    -- Same as previous but use "uri=" syntax to save URI value.
+    uri_set = uri.parse_many({
+        uri = "/tmp/unix.sock?q1=v1",
+        params = {q2 = "v2", q3 = {"v31", "v32"}}
+    })
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "table", "name")
+    test:is(#uri_set[1].params["q1"], 1, "value count")
+    test:is(uri_set[1].params["q1"][1], "v1", "param value")
+    test:is(type(uri_set[1].params["q2"]), "table", "name")
+    test:is(#uri_set[1].params["q2"], 1, "value count")
+    test:is(uri_set[1].params["q2"][1], "v2", "param value")
+    test:is(type(uri_set[1].params["q3"]), "table", "name")
+    test:is(#uri_set[1].params["q3"], 2, "value count")
+    test:is(uri_set[1].params["q3"][1], "v31", "param value")
+    test:is(uri_set[1].params["q3"][2], "v32", "param value")
+
+    -- Check that URI parameter value from "params" table
+    -- overwrite parameter value from the string.
+    uri_set = uri.parse_many({
+        "/tmp/unix.sock?q1=v1",
+        params = {q1 = {"v2", "v3"}}
+    })
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "table", "name")
+    test:is(#uri_set[1].params["q1"], 2, "value count")
+    -- "v1" value was overwriten by values from "params" table
+    test:is(uri_set[1].params["q1"][1], "v2", "param value")
+    test:is(uri_set[1].params["q1"][2], "v3", "param value")
+
+    -- Most common way: several URIs passed as array of strings
+    -- and objects with different parameters and default parameters.
+    uri_set = uri.parse_many({
+        "/tmp/unix.sock?q1=v11",
+        {  "/tmp/unix.sock?q2=v21", params = { q2 = "v22", q3 = "v31" } },
+        { "/tmp/unix.sock", params = { q1 = 1, q2 = { 2, 3, 4}, q3 = 5 } },
+        { uri = "/tmp/unix.sock?q2&q3=v31", params = { q3 = {"v33", "v34"} } },
+        default_params = {
+            q1 = {"v12", "v13"}, q2 = {"v21", "v22"}, q3 = {"v32"}, q4 = 6
+        }
+    })
+    test:is(#uri_set, 4, "uri count")
+    -- First URI from uri_set
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "table", "name")
+    test:is(#uri_set[1].params["q1"], 1, "value count")
+    -- As previously values from "default_params" table are
+    -- ignored if there is some values for this URI parameter
+    -- from URI string or "params" table.
+    test:is(uri_set[1].params["q1"][1], "v11", "param value")
+    test:is(type(uri_set[1].params["q2"]), "table", "name")
+    test:is(#uri_set[1].params["q2"], 2, "value count")
+    -- Values was added from "default_params" table.
+    test:is(uri_set[1].params["q2"][1], "v21", "param value")
+    test:is(uri_set[1].params["q2"][2], "v22", "param value")
+    test:is(type(uri_set[1].params["q3"]), "table", "name")
+    test:is(#uri_set[1].params["q3"], 1, "value count")
+    -- Value was added from "params" table.
+    test:is(uri_set[1].params["q3"][1], "v32", "param value")
+    test:is(type(uri_set[1].params["q4"]), "table", "name")
+    test:is(#uri_set[1].params["q4"], 1, "value count")
+    -- Numerical value, saved as a string.
+    test:is(uri_set[1].params["q4"][1], "6", "param value")
+    -- Second URI from uri_set
+    test:is(uri_set[2].host, 'unix/', 'host')
+    test:is(uri_set[2].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[2].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[2].params["q1"]), "table", "name")
+    test:is(#uri_set[2].params["q1"], 2, "value count")
+    -- Values from "default_params" table for "q1" parameter
+    -- are added, because there is no such parameter in URI
+    -- string and in "params" table.
+    test:is(uri_set[2].params["q1"][1], "v12", "param value")
+    test:is(uri_set[2].params["q1"][1], "v12", "param value")
+    test:is(type(uri_set[2].params["q2"]), "table", "name")
+    test:is(#uri_set[2].params["q2"], 1, "value count")
+    -- "q2" parameter value from URI string overwritten by
+    -- value from "params" table, values from "defaul_params"
+    -- table are ignored.
+    test:is(uri_set[2].params["q2"][1], "v22", "param value")
+    test:is(type(uri_set[2].params["q3"]), "table", "name")
+    test:is(#uri_set[2].params["q3"], 1, "value count")
+    -- "q3" parameter value added from from "params" table,
+    -- values from "defaul_params" table are ignored.
+    test:is(uri_set[2].params["q3"][1], "v31", "param value")
+    test:is(type(uri_set[2].params["q4"]), "table", "name")
+    test:is(#uri_set[2].params["q4"], 1, "value count")
+    -- Numerical value, saved as a string.
+    test:is(uri_set[2].params["q4"][1], "6", "param value")
+    -- Third URI from uri_set
+    -- All values are taken from "params" table, just check
+    -- how we parse numerical parameter values.
+    test:is(uri_set[3].host, 'unix/', 'host')
+    test:is(uri_set[3].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[3].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[3].params["q1"]), "table", "name")
+    test:is(#uri_set[3].params["q1"], 1, "value count")
+    test:is(uri_set[3].params["q1"][1], "1", "param value")
+    test:is(type(uri_set[3].params["q2"]), "table", "name")
+    test:is(#uri_set[3].params["q2"], 3, "value count")
+    test:is(uri_set[3].params["q2"][1], "2", "param value")
+    test:is(uri_set[3].params["q2"][2], "3", "param value")
+    test:is(uri_set[3].params["q2"][3], "4", "param value")
+    test:is(type(uri_set[3].params["q3"]), "table", "name")
+    test:is(#uri_set[3].params["q3"], 1, "value count")
+    test:is(uri_set[3].params["q3"][1], "5", "param value")
+    -- Fourth URI from uri_set
+    test:is(uri_set[4].host, 'unix/', 'host')
+    test:is(uri_set[4].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[4].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[4].params["q1"]), "table", "name")
+    test:is(#uri_set[4].params["q1"], 2, "value count")
+    -- As previous values was added from "default_params" table
+    test:is(uri_set[4].params["q1"][1], "v12", "param value")
+    test:is(uri_set[4].params["q1"][2], "v13", "param value")
+    test:is(type(uri_set[4].params["q2"]), "table", "name")
+    test:is(#uri_set[4].params["q2"], 2, "value count")
+    -- Values from "default_params" table for "q2" parameter
+    -- are added, because there is no values for this parameter
+    -- in URI string and there is no such paramater in "params
+    -- table.
+    test:is(uri_set[4].params["q2"][1], "v21", "param value")
+    test:is(uri_set[4].params["q2"][2], "v22", "param value")
+    test:is(type(uri_set[4].params["q3"]), "table", "name")
+    test:is(#uri_set[4].params["q3"], 2, "value count")
+    -- Value from URI string was overwritten by values from
+    -- "params" table. Values from "default_params" table are
+    -- ignored.
+    test:is(uri_set[4].params["q3"][1], "v33", "param value")
+    test:is(uri_set[4].params["q3"][2], "v34", "param value")
+    test:is(type(uri_set[4].params["q4"]), "table", "name")
+    test:is(#uri_set[4].params["q4"], 1, "value count")
+    -- Numerical value, saved as a string.
+    test:is(uri_set[4].params["q4"][1], "6", "param value")
+
+    -- If some URI parameter is a table in "params"
+    -- or "default_params" table, all keys in this
+    -- table should be numerical, otherwise silently
+    -- ignored
+    uri_set = uri.parse_many({
+        { "/tmp/unix.sock", params = {q1 = { x = "y"}} },
+        "/tmp/unix.sock",
+        default_params = {q2 = {x = "y"}}
+    })
+    test:is(#uri_set, 2, "uri count")
+    -- First URI from uri_set
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q1"]), "nil", "name")
+    test:is(type(uri_set[1].params["q2"]), "nil", "name")
+    test:is(uri_set[2].host, 'unix/', 'host')
+    test:is(uri_set[2].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[2].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[2].params["q1"]), "nil", "name")
+    test:is(type(uri_set[2].params["q2"]), "nil", "name")
+
+    -- URI table without URI
+    uri_set = uri.parse_many({
+        params = {q = "v"},
+        default_params = {}
+    })
+    test:is(#uri_set, 0, "uri count")
+
+    -- URI table with one URI in table format
+    uri_set = uri.parse_many{{"/tmp/unix.sock"}, default_params = {q = "v"}}
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q"]), "table", "name")
+    test:is(#uri_set[1].params["q"], 1, "value count")
+    test:is(uri_set[1].params["q"][1], "v", "param value")
+
+    -- Same as previous but with "uri=" syntax
+    uri_set = uri.parse_many{{uri = "/tmp/unix.sock"}, default_params = {q = "v"}}
+    test:is(#uri_set, 1, "uri count")
+    test:is(uri_set[1].host, 'unix/', 'host')
+    test:is(uri_set[1].service, '/tmp/unix.sock', 'service')
+    test:is(uri_set[1].unix, '/tmp/unix.sock', 'unix')
+    test:is(type(uri_set[1].params["q"]), "table", "name")
+    test:is(#uri_set[1].params["q"], 1, "value count")
+    test:is(uri_set[1].params["q"][1], "v", "param value")
+end
+
+local function test_parse_invalid_uri_set_from_lua_table(test)
+    -- Tests for uri.parse_many() Lua bindings.
+    -- (Several invalid URIs with parameters, passed in different ways).
+    test:plan(50)
+
+    local uri_set, error, expected_errmsg
+
+    -- Invalid type passed to "parse_many"
+    expected_errmsg = "Incorrect type for URI: should be string, number or table"
+    uri_set, error = uri.parse_many(function() end)
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Invalid type of value for numerical key
+    expected_errmsg = "Incorrect type for URI: should be string, number or table"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", function() end})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Invalid type of value for string keys
+    expected_errmsg = "Invalid URI table: expected " ..
+                      "{uri = string, params = table}" .. " or " ..
+                      "{string, params = table}"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", uri = function() end})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Incorrect type for URI parameters: should be a table"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", params = function() end})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Incorrect type for URI parameters: should be a table"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", params = ""})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", default_params = ""})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", default_params = ""})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+
+    -- Mix "uri=" and numerical keys is banned
+    expected_errmsg = "Invalid URI table: expected " ..
+                      "{uri = string, params = table}" .. " or " ..
+                      "{string, params = table}"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", uri = "/tmp/unix.sock"})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Several URIs in one string is allowed only when the
+    -- passed as a single string.
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock, /tmp/unix.sock"})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- "params" table is allowed only for single URI
+    expected_errmsg = "URI parameters are not allowed for multiple URIs"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock", "/tmp/unix.sock",
+        params = {q1 = "v1"}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- "params" table is not allowed with nested tables
+    expected_errmsg = "URI parameters are not allowed for multiple URIs"
+    uri_set, error = uri.parse_many({
+        {"/tmp/unix.sock"},
+        params = {q1 = "v1"}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- "default_params" table is not allowed in nested URI tables
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({{"/tmp/unix.sock", default_params = {}}})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- "default_params" table is not allowed for single URI
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({"/tmp/unix.sock", default_params = {}})
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Only one URI is allowed in nested tables
+    expected_errmsg = "Invalid URI table: expected " ..
+                      "{uri = string, params = table}" .. " or " ..
+                      "{string, params = table}"
+    uri_set, error = uri.parse_many({
+        {"/tmp/unix.sock", "/tmp/unix.sock"},
+        default_params = {q = "v"}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Nested URI tables is not allowed in nested tables
+    expected_errmsg = "Invalid URI table: expected "..
+                      "{uri = string, params = table}" .. " or " ..
+                      "{string, params = table}"
+    uri_set, error = uri.parse_many({
+        {"/tmp/unix.sock", {}}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Nested URI table without URI is now allowed
+    expected_errmsg = "Invalid URI table: expected "..
+                      "{uri = string, params = table}" .. " or " ..
+                      "{string, params = table}"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        { params = {q = "v"} }
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Only string key types are allowed in "params" and
+    -- "default_params" table
+    expected_errmsg = "Incorrect type for URI parameter name: " ..
+                      "should be a string"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        params = {"v"},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        default_params = {"v"},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Invalid type of values in "params" and
+    -- "default_params" table
+    expected_errmsg = "Incorrect type for URI parameter value: " ..
+                      "should be string, number or table"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        params = {q = function() end},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        default_params = {q = function() end},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Incorrect type for URI parameter value: "..
+                      "should be string or number"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        params = {q = {function() end}},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    expected_errmsg = "Default URI parameters are not allowed for single URI"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        default_params = {q = {function() end}},
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Invalid uri string in URIs table
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        "://"
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Invalid uri in nested URI table
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        {"://"}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
+    -- Same as previous but with "uri=" syntax
+    expected_errmsg = "Incorrect URI: expected host:service or /unix.socket"
+    uri_set, error = uri.parse_many({
+        "/tmp/unix.sock",
+        {uri = "://"}
+    })
+    test:isnil(uri_set, "invalid uri", uri_set)
+    test:is(tostring(error), expected_errmsg, "error message")
 end
 
 tap.test("uri", function(test)
-    test:plan(4)
+    test:plan(6)
     test:test("parse", test_parse)
     test:test("parse URI query params", test_parse_uri_query_params)
     test:test("parse URIs with query params", test_parse_uri_set_with_query_params)
+    test:test("parse URIs from lua table", test_parse_uri_set_from_lua_table)
+    test:test("parse invalid URIs from lua table", test_parse_invalid_uri_set_from_lua_table)
     test:test("format", test_format)
 end)
-- 
GitLab