1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
local M = {}
---@class vim.net.request.Opts
---@inlinedoc
---
---Enables verbose output.
---@field verbose? boolean
---
---Number of retries on transient failures (default: 3).
---@field retry? integer
---
---File path to save the response body to.
---@field outpath? string
---
---Buffer to save the response body to.
---@field outbuf? integer
---
---Custom headers to send with the request. Supports basic key/value headers and empty headers as
---supported by curl. Does not support "@filename" style, internal header deletion ("Header:").
---@field headers? table<string, string>
---@class vim.net.request.Response
---
---The HTTP body of the request
---@field body string
--- Makes an HTTP GET request to the given URL, asynchronously passing the result to the specified
--- `on_response`, `outpath` or `outbuf`.
---
--- Examples:
--- ```lua
--- -- Write response body to file.
--- vim.net.request('https://neovim.io/charter/', {
--- outpath = 'vision.html',
--- })
---
--- -- Process the response.
--- vim.net.request(
--- 'https://api.github.com/repos/neovim/neovim',
--- {},
--- function (err, res)
--- if err then return end
--- local stars = vim.json.decode(res.body).stargazers_count
--- vim.print(('Neovim currently has %d stars'):format(stars))
--- end
--- )
---
--- -- Write to both file and current buffer, but cancel it.
--- local job = vim.net.request('https://neovim.io/charter/', {
--- outpath = 'vision.html',
--- outbuf = 0,
--- })
--- job:close()
---
--- -- Add custom headers in the request.
--- vim.net.request('https://neovim.io/charter/', {
--- headers = { Authorization = 'Bearer XYZ' },
--- })
--- ```
---
--- @param url string The URL for the request.
--- @param opts? vim.net.request.Opts
--- @param on_response? fun(err: string?, response: vim.net.request.Response?)
--- Callback invoked on request completion. The `body` field in the response
--- parameter contains the raw response data (text or binary).
--- @return { close: fun() } # Object with `close()` method which cancels the request.
function M.request(url, opts, on_response)
vim.validate('url', url, 'string')
vim.validate('opts', opts, 'table', true)
vim.validate('on_response', on_response, 'function', true)
opts = opts or {}
local retry = opts.retry or 3
-- Build curl command
local args = { 'curl' }
if opts.verbose then
table.insert(args, '--verbose')
else
vim.list_extend(args, { '--silent', '--show-error', '--fail' })
end
vim.list_extend(args, { '--location', '--retry', tostring(retry) })
if opts.outpath then
vim.list_extend(args, { '--output', opts.outpath })
end
if opts.headers then
vim.validate('opts.headers', opts.headers, 'table', true)
for key, value in pairs(opts.headers) do
if type(key) ~= 'string' or type(value) ~= 'string' then
error('headers keys and values must be strings')
end
if key:match(':$') or key:match(';$') or key:match('^@') then
error('header keys must not start with @ or end with : and ;')
end
if value == '' then
vim.list_extend(args, { '-H', key .. ';' })
else
vim.list_extend(args, { '-H', key .. ': ' .. value })
end
end
end
table.insert(args, url)
local job = vim.system(args, {}, function(res)
---@type string?, vim.net.request.Response?
local err, response = nil, nil
if res.signal ~= 0 then
err = ('Request killed with signal %d'):format(res.signal)
elseif res.code ~= 0 then
err = res.stderr ~= '' and res.stderr or ('Request failed with exit code %d'):format(res.code)
else
if on_response then
response = { body = res.stdout or '' }
end
end
-- nvim_buf_is_loaded and nvim_buf_set_lines are not allowed in fast context
vim.schedule(function()
if res.code == 0 and opts.outbuf and vim.api.nvim_buf_is_loaded(opts.outbuf) then
local lines = vim.split(res.stdout, '\n', { plain = true })
vim.api.nvim_buf_set_lines(opts.outbuf, 0, -1, true, lines)
end
end)
if on_response then
on_response(err, response)
end
end)
return {
close = function()
job:kill('sigint')
end,
}
end
return M
|