Skip to content

Commit

Permalink
httpc: introduce format_query and parse_query functions
Browse files Browse the repository at this point in the history
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
...

```
  • Loading branch information
olegrok committed Jan 1, 2020
1 parent 360e3a3 commit 86c6e75
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 2 deletions.
60 changes: 59 additions & 1 deletion src/lua/httpc.lua
Expand Up @@ -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)
Expand Down
39 changes: 38 additions & 1 deletion test/app-tap/http_client.test.lua
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

0 comments on commit 86c6e75

Please sign in to comment.