[Tarantool-patches] [PATCH 2/2] httpc: introduce format_query and parse_query functions

olegrok at tarantool.org olegrok at tarantool.org
Thu Jan 9 11:04:14 MSK 2020


From: Oleg Babin <babinoleg at mail.ru>

Previous patch introduced url_escape and url_unescape.
However it's quite poorly to give our users too
low-level functions. The main use case is to form and parse
http query parameters

This patch introduces format_query and parse_query functions

Closes #3682

@TarantoolBot document
Title: New http.client functions

Four new functions are available in http.client module:

  - url_escape - simple wrapper over curl_easy_escape
      (https://curl.haxx.se/libcurl/c/curl_easy_escape.html)

  - url_unescape - simple wrapper over curl_easy_unescape
      (https://curl.haxx.se/libcurl/c/curl_easy_unescape.html)

Examples:
```lua
-- According RFC3986

tarantool> httpc.url_escape('hello')
---
- hello
...

tarantool> httpc.url_escape('Привет')
---
- '%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82'
...

tarantool> httpc.url_escape('$&+,:;=?@-._~')
---
- '%24%26%2B%2C%3A%3B%3D%3F%40-._~'
...
```

  - format_query - format query arguments from key-value string pairs
for HTTP request

  - parse_query - parse query string into table

Example:
```lua
-- The query string is composed of a series of field-value pairs
-- Within each pair, the field name and value are
--   separated by an equals sign, "="
-- The series of pairs is separated by the ampersand, "&"

tarantool> httpc.format_query({['hello'] = 'world', ['привет'] = 'мир'})
---
- '%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80&hello=world'
...

tarantool> httpc.parse_query('%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=' ..
'%D0%BC%D0%B8%D1%80&hello=world')
---
- привет: мир
  hello: world
...

```
---
Issue: https://github.com/tarantool/tarantool/issues/3682
 src/lua/httpc.lua                 | 60 ++++++++++++++++++++++++++++++-
 test/app-tap/http_client.test.lua | 39 +++++++++++++++++++-
 2 files changed, 97 insertions(+), 2 deletions(-)

diff --git a/src/lua/httpc.lua b/src/lua/httpc.lua
index 33cb78f3c..7d40b3bcc 100644
--- a/src/lua/httpc.lua
+++ b/src/lua/httpc.lua
@@ -449,10 +449,68 @@ local function url_unescape(str)
     return driver.unescape(http_default.curl, str)
 end
 
+--
+--  This function format query params from table with arguments
+--
+--  Parameters:
+--
+--  args  -  table with key-value pairs that will be
+--               encoded as string "key1=value1&key2=value2"
+--               each element of pair is encoded with url_escape
+--
+local function format_query(args)
+    if type(args) ~= "table" then
+        error("httpc.format_query expected table with args")
+    end
+    local encoded = {}
+    for k, v in pairs(args) do
+        k = tostring(k)
+        v = tostring(v)
+        k = url_escape(k)
+        v = url_escape(v)
+        table.insert(encoded, k .. '=' .. v)
+    end
+
+    return table.concat(encoded, '&')
+end
+
+--
+--  This function parse query params from string to table
+--
+--  Parameters:
+--
+--  str  -  string in format "key1=value1&key2=value2"
+--              that will be parsed into {key1 = value1, key2 = value2}
+--              each element of pair is decoded with url_unescape
+--
+local function parse_query(str)
+    if type(str) ~= "string" then
+        error("httpc.parse_query expected string with args")
+    end
+    local args = string.split(str, '&')
+
+    local decoded = {}
+    for _, arg in ipairs(args) do
+        local k_v = string.split(arg, '=', 1)
+        if #k_v ~= 2 then
+            error("httpc.parse_query expected arguments in format 'key=value'"
+                .. " separated by '&', got: " .. tostring(arg))
+        end
+        local key = url_unescape(k_v[1])
+        local value = url_unescape(k_v[2])
+
+        decoded[key] = value
+    end
+
+    return decoded
+end
+
 local this_module = {
     new = http_new,
     url_escape = url_escape,
-    url_unescape = url_unescape
+    url_unescape = url_unescape,
+    format_query = format_query,
+    parse_query = parse_query,
 }
 
 local function http_default_wrap(fname)
diff --git a/test/app-tap/http_client.test.lua b/test/app-tap/http_client.test.lua
index 3676f56b4..4b5e6bbc5 100755
--- a/test/app-tap/http_client.test.lua
+++ b/test/app-tap/http_client.test.lua
@@ -611,7 +611,7 @@ function run_tests(test, sock_family, sock_addr)
     stop_server(test, server)
 end
 
-test:plan(3)
+test:plan(5)
 
 test:test("http over AF_INET", function(test)
     local s = socketlib('AF_INET', 'SOCK_STREAM', 0)
@@ -653,4 +653,41 @@ test:test("url_escape/url_unescape", function(test)
     test:is('-._~', client.url_escape('-._~'), '-._~ are not escaped according RFC 3986')
 end)
 
+test:test("format_query", function(test)
+    test:plan(6)
+    test:is(client.format_query({['hello'] = 'world'}), 'hello=world', 'correct formation for one eng arg')
+    test:is(client.format_query({['привет'] = 'мир'}),
+        '%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80',  'correct formation for one rus arg')
+    test:is(client.format_query({['key=key'] = 'value;value'}), 'key%3Dkey=value%3Bvalue',
+        'correct formation for args with special characters')
+    test:is(client.format_query({['key1'] = 'value1', ['ключ2'] = 'значение'}),
+        'key1=value1&%D0%BA%D0%BB%D1%8E%D1%872=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5',
+        'correct formation for several args')
+
+    local ok, err = pcall(client.format_query, 'str')
+    test:is(ok, false, 'only table could be passed as argument')
+    test:ok(err:endswith('httpc.format_query expected table with args'), 'correct error message')
+end)
+
+test:test("parse_query", function(test)
+    test:plan(8)
+        test:is_deeply(client.parse_query('hello=world'), {['hello'] = 'world'}, 'correct parsing for one eng arg')
+        test:is_deeply(client.parse_query('%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80'),
+        {['привет'] = 'мир'}, 'correct parsing for one rus arg')
+        test:is_deeply(client.parse_query('key%3Dkey=value%3Bvalue'), {['key=key'] = 'value;value'},
+            'correct formation for args with special characters')
+        test:is_deeply(client.parse_query(
+            'key1=value1&%D0%BA%D0%BB%D1%8E%D1%872=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5'),
+            {['key1'] = 'value1', ['ключ2'] = 'значение'}, 'correct parsing for several args')
+
+        local ok, err = pcall(client.parse_query, {})
+        test:is(ok, false, 'only string could be passed as argument')
+        test:ok(err:endswith('httpc.parse_query expected string with args'), 'correct error message')
+
+        local ok, err = pcall(client.parse_query, '1=2&3&4')
+        test:is(ok, false, 'unexpected input format error')
+        test:ok(err:endswith('httpc.parse_query expected arguments in format \'key=value\'' ..
+            ' separated by \'&\', got: 3'), 'correct error message')
+end)
+
 os.exit(test:check() == true and 0 or -1)
-- 
2.23.0



More information about the Tarantool-patches mailing list