Skip to content

Commit bf41cbf

Browse files
feat: client selector & libcurl client
1 parent 2b61cec commit bf41cbf

File tree

6 files changed

+175
-90
lines changed

6 files changed

+175
-90
lines changed

lua/rest-nvim/api.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ local api = {}
1111

1212
local autocmds = require("rest-nvim.autocmds")
1313
local commands = require("rest-nvim.commands")
14+
local client = require("rest-nvim.client")
1415

1516
---rest.nvim API version, equals to the current rest.nvim version. Meant to be used by modules later
1617
---@type string
@@ -42,4 +43,8 @@ function api.register_rest_subcommand(name, cmd)
4243
commands.register_subcommand(name, cmd)
4344
end
4445

46+
function api.register_rest_client(c)
47+
client.register_client(c)
48+
end
49+
4550
return api

lua/rest-nvim/client/curl/libcurl.lua

Lines changed: 71 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ local utils = require("rest-nvim.utils")
1616
local logger = require("rest-nvim.logger")
1717
local config = require("rest-nvim.config")
1818
local curl_utils = require("rest-nvim.client.curl.utils")
19-
20-
-- TODO: add support for submitting forms in the `client.request` function
19+
local curl_cli = require("rest-nvim.client.curl.cli")
2120

2221
---Get request statistics
2322
---@param req table cURL request class
@@ -46,7 +45,6 @@ local function get_stats(req, statistics_tbl)
4645
local stats = {}
4746

4847
for name, _ in pairs(statistics_tbl) do
49-
-- stats[name] = style.title .. " " .. get_stat(req, name)
5048
stats[name] = get_stat(req, name)
5149
end
5250

@@ -55,33 +53,33 @@ end
5553

5654
---Execute an HTTP request using cURL
5755
---return return nil if execution failed
58-
---@param request rest.Request Request data to be passed to cURL
59-
---@return table? info The request information (url, method, headers, body, etc)
60-
function client.request_(request)
61-
logger.info("sending request to: " .. request.url)
62-
-- write to `Context.response` without altering the reference
63-
local info = request.context.aesponse
56+
---@param req rest.Request Request data to be passed to cURL
57+
---@return rest.Response? info The request information (url, method, headers, body, etc)
58+
function client.request(req)
59+
logger.info("sending request to: " .. req.url)
6460
if not found_curl then
6561
---@diagnostic disable-next-line need-check-nil
6662
logger.error("lua-curl could not be found, therefore the cURL client will not work.")
6763
return
6864
end
69-
local host = request.headers["host"]
65+
local host = req.headers["host"]
7066
if host then
71-
request.url = host .. request.url
67+
req.url = host .. req.url
7268
end
7369

7470
-- We have to concat request headers to a single string, e.g. ["Content-Type"]: "application/json" -> "Content-Type: application/json"
7571
local headers = {}
76-
for name, value in pairs(request.headers) do
77-
table.insert(headers, name .. ": " .. value)
72+
for name, values in pairs(req.headers) do
73+
for _, value in ipairs(values) do
74+
table.insert(headers, name .. ": " .. value)
75+
end
7876
end
7977

8078
-- Whether to skip SSL host and peer verification
8179
local skip_ssl_verification = config.skip_ssl_verification
82-
local req = curl.easy_init()
83-
req:setopt({
84-
url = request.url,
80+
local req_ = curl.easy_init()
81+
req_:setopt({
82+
url = req.url,
8583
-- verbose = true,
8684
httpheader = headers,
8785
ssl_verifyhost = skip_ssl_verification,
@@ -92,89 +90,100 @@ function client.request_(request)
9290
local should_encode_url = config.encode_url
9391
if should_encode_url then
9492
-- Create a new URL as we cannot extract the URL from the req object
95-
local _url = curl.url()
96-
_url:set_url(request.url)
93+
local url_ = curl.url()
94+
url_:set_url(req.url)
9795
-- Re-add the request query with the encoded parameters
98-
local query = _url:get_query()
96+
local query = url_:get_query()
9997
if type(query) == "string" then
100-
_url:set_query("")
98+
url_:set_query("")
10199
for param in vim.gsplit(query, "&") do
102-
_url:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY)
100+
url_:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY)
103101
end
104102
end
105103
-- Re-add the request URL to the req object
106-
req:setopt_url(_url:get_url())
104+
req_:setopt_url(url_:get_url())
107105
end
108106

109107
-- Set request HTTP version, defaults to HTTP/1.1
110-
if request.http_version then
111-
local http_version = request.http_version:gsub("%.", "_")
112-
req:setopt_http_version(curl["HTTP_VERSION_" .. http_version])
108+
if req.http_version then
109+
local http_version = req.http_version:gsub("%.", "_")
110+
req_:setopt_http_version(curl["HTTP_VERSION_" .. http_version])
113111
else
114-
req:setopt_http_version(curl.HTTP_VERSION_1_1)
112+
req_:setopt_http_version(curl.HTTP_VERSION_1_1)
115113
end
116114

117115
-- If the request method is not GET then we have to build the method in our own
118116
-- See: https://github.com/Lua-cURL/Lua-cURLv3/issues/156
119-
local method = request.method
117+
local method = req.method
120118
if vim.tbl_contains({ "POST", "PUT", "PATCH", "TRACE", "OPTIONS", "DELETE" }, method) then
121-
req:setopt_post(true)
122-
req:setopt_customrequest(method)
119+
req_:setopt_post(true)
120+
req_:setopt_customrequest(method)
123121
end
124122

125123
-- local body = vim.deepcopy(request.body)
126-
if request.body then
127-
if request.body.__TYPE == "json" then
128-
req:setopt_postfields(request.body.data)
129-
elseif request.body.__TYPE == "xml" then
130-
req:setopt_postfields(request.body.data)
131-
elseif request.body.__TYPE == "external" then
124+
if req.body then
125+
if req.body.__TYPE == "json" then
126+
req_:setopt_postfields(req.body.data)
127+
elseif req.body.__TYPE == "xml" then
128+
req_:setopt_postfields(req.body.data)
129+
elseif req.body.__TYPE == "external" then
132130
local mimetypes = require("mimetypes")
133-
local body_mimetype = mimetypes.guess(request.body.data.path)
131+
local body_mimetype = mimetypes.guess(req.body.data.path)
134132
local post_data = {
135-
[request.body.data.name and request.body.data.name or "body"] = {
136-
file = request.body.data.path,
133+
[req.body.data.name and req.body.data.name or "body"] = {
134+
file = req.body.data.path,
137135
type = body_mimetype,
138136
},
139137
}
140-
req:post(post_data)
141-
elseif request.body.__TYPE == "form" then
138+
req_:post(post_data)
139+
elseif req.body.__TYPE == "form" then
142140
local form = curl.form()
143-
for k, v in pairs(request.body.data) do
141+
for k, v in pairs(req.body.data) do
144142
form:add_content(k, v)
145143
end
146-
req:setopt_httppost(form)
144+
req_:setopt_httppost(form)
147145
else
148-
logger.error(("'%s' type body is not supported yet"):format(request.body.__TYPE))
146+
logger.error(("'%s' type body is not supported yet"):format(req.body.__TYPE))
149147
return
150148
end
151149
end
152150

153151
-- Request execution
154152
local res_result = {}
155-
local res_headers = {}
156-
req:setopt_writefunction(table.insert, res_result)
157-
req:setopt_headerfunction(table.insert, res_headers)
158-
159-
local ok, err = req:perform()
160-
if ok then
161-
-- Get request statistics if they are enabled
162-
local stats_config = config.result.behavior.statistics
163-
if stats_config.enable then
164-
info.statistics = get_stats(req, stats_config.stats)
165-
end
153+
---@type table<string, string[]>
154+
local res_raw_headers = {}
155+
req_:setopt_writefunction(table.insert, res_result)
156+
req_:setopt_headerfunction(table.insert, res_raw_headers)
166157

167-
info.url = req:getinfo_effective_url()
168-
info.code = req:getinfo_response_code()
169-
info.method = req:getinfo_effective_method()
170-
info.headers = table.concat(res_headers):gsub("\r", "")
171-
info.body = table.concat(res_result)
172-
else
158+
local ok, err = req_:perform()
159+
if not ok then
173160
logger.error("Something went wrong when making the request with cURL:\n" .. curl_utils.curl_error(err:no()))
174161
return
175162
end
176-
req:close()
177-
return info
163+
---@diagnostic disable-next-line: invisible
164+
local status = curl_cli.parser.parse_verbose_status(table.remove(res_raw_headers, 1))
165+
local res_headers = {}
166+
for _, header in ipairs(res_raw_headers) do
167+
---@diagnostic disable-next-line: invisible
168+
local key, value = curl_cli.parser.parse_header_pair(header)
169+
if key then
170+
if not res_headers[key] then
171+
res_headers[key] = {}
172+
end
173+
table.insert(res_headers[key], value)
174+
end
175+
end
176+
---@type rest.Response
177+
local res = {
178+
status = status,
179+
headers = res_headers,
180+
body = table.concat(res_result),
181+
statistics = get_stats(req_, {})
182+
}
183+
logger.debug(vim.inspect(res.headers))
184+
res.status.text = vim.trim(res.status.text)
185+
req_:close()
186+
return res
178187
end
179188

180189
return client

lua/rest-nvim/client/curl_cli.lua

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
local curl_cli = require("rest-nvim.client.curl.cli")
22

3+
local COMPATIBLE_METHODS = {
4+
"OPTIONS",
5+
"GET",
6+
"HEAD",
7+
"POST",
8+
"PUT",
9+
"DELETE",
10+
"TRACE",
11+
"CONNECT",
12+
"PATCH",
13+
"LIST",
14+
}
15+
316
---@type rest.Client
417
local client = {
5-
request = curl_cli.request
18+
name = "curl_cli",
19+
request = curl_cli.request,
20+
available = function (req)
21+
local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method)
22+
local url_ok = req.url:match("^https?://")
23+
return method_ok and url_ok
24+
end
625
}
726

827
return client

lua/rest-nvim/client/init.lua

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
---@mod rest-nvim.client rest.nvim client module
2-
---
3-
---@brief [[
4-
---
5-
--- Mainly about `rest.Client` type requirements
6-
---
7-
---@brief ]]
2+
3+
local clients = {}
84

95
---Client to send request
106
---@class rest.Client
11-
local client = {}
12-
7+
---@field name string name of the client
138
---Sends request and return the response asynchronously
9+
---@field request fun(req: rest.Request):nio.control.Future
10+
---Check if client can handle given request
11+
---@field available fun(req: rest.Request):boolean
12+
13+
clients.clients = {
14+
require("rest-nvim.client.curl_cli"),
15+
-- require("rest-nvim.client.libcurl"),
16+
}
17+
18+
function clients.register_client(client)
19+
vim.validate({
20+
client = {
21+
client,
22+
function (c)
23+
return type(c) == "table" and type(c.request) == "function" and type(c.available) == "function"
24+
end,
25+
"table with `name`, `request()` and `available()` fields"
26+
}
27+
})
28+
table.insert(clients.clients, client)
29+
end
30+
31+
---Find all registered clients available for given request
1432
---@param req rest.Request
15-
---@return nio.control.Future future Future containing `rest.Response`
16-
function client:request(req)
17-
local future = require("nio").control.future()
18-
vim.print(self, req)
19-
return future
33+
---@return rest.Client[]
34+
function clients.get_available_clients(req)
35+
return vim.tbl_filter(function (c)
36+
return c.available(req)
37+
end, clients.clients)
2038
end
2139

22-
return client
40+
return clients

lua/rest-nvim/client/libcurl.lua

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
local libcurl = require("rest-nvim.client.curl.libcurl")
2+
3+
local nio = require("nio")
4+
5+
local COMPATIBLE_METHODS = {
6+
"OPTIONS",
7+
"GET",
8+
"HEAD",
9+
"POST",
10+
"PUT",
11+
"DELETE",
12+
"TRACE",
13+
"CONNECT",
14+
"PATCH",
15+
"LIST",
16+
}
17+
18+
local COMPATIBLE_BODY_TYPES = {
19+
"json",
20+
"xml",
21+
"external",
22+
"form",
23+
}
24+
25+
---@type rest.Client
26+
local client = {
27+
name = "libcurl",
28+
request = function (req)
29+
local res = libcurl.request(req)
30+
local future = nio.control.future()
31+
future.set(res)
32+
return future
33+
end,
34+
available = function (req)
35+
local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method)
36+
local url_ok = req.url:match("^https?://")
37+
local body_ok = (not req.body) or vim.list_contains(COMPATIBLE_BODY_TYPES, req.body.__TYPE)
38+
return method_ok and url_ok and body_ok
39+
end
40+
}
41+
42+
return client

lua/rest-nvim/request.lua

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local config = require("rest-nvim.config")
99
local ui = require("rest-nvim.ui.result")
1010
local nio = require("nio")
1111
local jar = require("rest-nvim.cookie_jar")
12+
local clients = require("rest-nvim.client")
1213

1314
---@class rest.Request.Body
1415
---@field __TYPE "json"|"xml"|"external"|"form"|"graphql"
@@ -30,19 +31,10 @@ local rest_nvim_last_request = nil
3031
---@param req rest.Request
3132
local function run_request(req)
3233
logger.debug("run_request")
33-
---@type rest.Client
34-
local client
35-
if req.method == "WEBSOCKET" then
36-
logger.error("method: websocket isn't supported yet")
37-
return
38-
elseif req.method == "GRPC" then
39-
logger.error("method: grpc isn't supported yet")
40-
return
41-
elseif req.method == "GRAPHQL" then
42-
logger.error("method: graphql isn't supported yet")
43-
return
44-
else
45-
client = require("rest-nvim.client.curl.cli")
34+
local client = clients.get_available_clients(req)[1]
35+
if not client then
36+
logger.error("can't find registered client available for request:\n" .. vim.inspect(req))
37+
vim.notify("[rest.nvim] Can't find registered client available for request", vim.log.levels.ERROR)
4638
end
4739
rest_nvim_last_request = req
4840

0 commit comments

Comments
 (0)