diff --git a/.editorconfig b/.editorconfig index 1ca5c72..41db816 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,9 +8,6 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.lua] -indent_size = 2 - [*.{scm,nix}] indent_size = 2 diff --git a/doc/rest-nvim.txt b/doc/rest-nvim.txt index 876ade9..996ceff 100644 --- a/doc/rest-nvim.txt +++ b/doc/rest-nvim.txt @@ -60,9 +60,9 @@ rest.nvim commands *rest-nvim.commands* copying command to clipboard, directly insert curl command as a comment. - NOTE: All `:Rest` commands opening new window support |command-modifiers|. - For example, you can run `:hor Rest open` to open result pane in horizontal - split +NOTE: All `:Rest` commands opening new window support |command-modifiers|. +For example, you can run `:hor Rest open` to open result pane in horizontal +split or run `:tab Rest logs` to open logs file in a new tab. ============================================================================== diff --git a/ftdetect/http.lua b/ftdetect/http.lua index 4cd204f..06c2c89 100644 --- a/ftdetect/http.lua +++ b/ftdetect/http.lua @@ -1,5 +1,5 @@ vim.filetype.add({ - extension = { - http = "http", - }, + extension = { + http = "http", + }, }) diff --git a/lua/lualine/components/rest.lua b/lua/lualine/components/rest.lua index 692064d..05efd1c 100644 --- a/lua/lualine/components/rest.lua +++ b/lua/lualine/components/rest.lua @@ -2,29 +2,29 @@ local lualine_require = require("lualine_require") local M = lualine_require.require("lualine.component"):extend() local default_options = { - fg = "#428890", - icon = "", + fg = "#428890", + icon = "", } function M:init(options) - M.super.init(self, options) - self.options = vim.tbl_deep_extend("keep", self.options or {}, default_options) - self.icon = self.options.icon + M.super.init(self, options) + self.options = vim.tbl_deep_extend("keep", self.options or {}, default_options) + self.icon = self.options.icon - self.highlight_color = self:create_hl({ fg = self.options.fg }, "Rest") + self.highlight_color = self:create_hl({ fg = self.options.fg }, "Rest") end function M:apply_icon() - local default_highlight = self:get_default_hl() - self.status = self:format_hl(self.highlight_color) .. self.icon .. " " .. default_highlight .. self.status + local default_highlight = self:get_default_hl() + self.status = self:format_hl(self.highlight_color) .. self.icon .. " " .. default_highlight .. self.status end function M.update_status() - local current_filetype = vim.bo.filetype - if current_filetype == "http" then - return vim.b._rest_nvim_env_file - end - return "" + local current_filetype = vim.bo.filetype + if current_filetype == "http" then + return vim.b._rest_nvim_env_file + end + return "" end return M diff --git a/lua/rest-nvim/api.lua b/lua/rest-nvim/api.lua index ca94b0a..f8e7073 100644 --- a/lua/rest-nvim/api.lua +++ b/lua/rest-nvim/api.lua @@ -31,20 +31,20 @@ api.namespace = vim.api.nvim_create_namespace("rest-nvim") ---@param cb string|fun(args: table) Autocommand lua callback, runs a Vimscript command instead if it is a `string` ---@param description string Autocommand description function api.register_rest_autocmd(events, cb, description) ----@diagnostic disable-next-line: invisible - autocmds.register_autocmd(events, cb, description) + ---@diagnostic disable-next-line: invisible + autocmds.register_autocmd(events, cb, description) end ---Register a new `:Rest` subcommand ---@param name string The name of the subcommand to register ---@param cmd RestCmd function api.register_rest_subcommand(name, cmd) ----@diagnostic disable-next-line: invisible - commands.register_subcommand(name, cmd) + ---@diagnostic disable-next-line: invisible + commands.register_subcommand(name, cmd) end function api.register_rest_client(c) - client.register_client(c) + client.register_client(c) end return api diff --git a/lua/rest-nvim/autocmds.lua b/lua/rest-nvim/autocmds.lua index a21acab..f9301af 100644 --- a/lua/rest-nvim/autocmds.lua +++ b/lua/rest-nvim/autocmds.lua @@ -1,6 +1,4 @@ ----@mod rest-nvim.autocmds rest.nvim autocommands ---- ----@mod rest-nvim.autocmds rest.nvim autocommands +---@mod rest-nvim.events rest.nvim user events --- ---@brief [[ --- @@ -48,51 +46,51 @@ local autocmds = {} ---Set up Rest autocommands group ---@package function autocmds.setup() - vim.api.nvim_create_augroup("Rest", { clear = true }) + vim.api.nvim_create_augroup("Rest", { clear = true }) - vim.api.nvim_create_autocmd("User", { - pattern = "RestRequestPre", - callback = function (_ev) - local config = require("rest-nvim.config") - local utils = require("rest-nvim.utils") - local req = _G.rest_request - local hooks = config.request.hooks - if hooks.encode_url then - req.url = utils.escape(req.url, true) - end - if hooks.user_agent ~= "" then - local header_empty = not req.headers["user-agent"] or #req.headers["user-agent"] < 1 - if header_empty then - local user_agent = type(hooks.user_agent) == "function" and hooks.user_agent() or hooks.user_agent - ---@cast user_agent string - req.headers["user-agent"] = { user_agent } - end - end - if hooks.set_content_type then - local header_empty = not req.headers["content-type"] or #req.headers["content-type"] < 1 - if header_empty and req.body then - if req.body.__TYPE == "json" then - req.headers["content-type"] = { "application/json" } - elseif req.body.__TYPE == "xml" then - req.headers["content-type"] = { "application/xml" } - -- TODO: auto-set content-type header for external body - end - end - end - end - }) - vim.api.nvim_create_autocmd("User", { - pattern = "RestResponsePre", - callback = function (_ev) - local config = require("rest-nvim.config") - local utils = require("rest-nvim.utils") - local req = _G.rest_request - local _res = _G.rest_response - if config.response.hooks.decode_url then - req.url = utils.url_decode(req.url) - end - end - }) + vim.api.nvim_create_autocmd("User", { + pattern = "RestRequestPre", + callback = function(_ev) + local config = require("rest-nvim.config") + local utils = require("rest-nvim.utils") + local req = _G.rest_request + local hooks = config.request.hooks + if hooks.encode_url then + req.url = utils.escape(req.url, true) + end + if hooks.user_agent ~= "" then + local header_empty = not req.headers["user-agent"] or #req.headers["user-agent"] < 1 + if header_empty then + local user_agent = type(hooks.user_agent) == "function" and hooks.user_agent() or hooks.user_agent + ---@cast user_agent string + req.headers["user-agent"] = { user_agent } + end + end + if hooks.set_content_type then + local header_empty = not req.headers["content-type"] or #req.headers["content-type"] < 1 + if header_empty and req.body then + if req.body.__TYPE == "json" then + req.headers["content-type"] = { "application/json" } + elseif req.body.__TYPE == "xml" then + req.headers["content-type"] = { "application/xml" } + -- TODO: auto-set content-type header for external body + end + end + end + end, + }) + vim.api.nvim_create_autocmd("User", { + pattern = "RestResponsePre", + callback = function(_ev) + local config = require("rest-nvim.config") + local utils = require("rest-nvim.utils") + local req = _G.rest_request + local _res = _G.rest_response + if config.response.hooks.decode_url then + req.url = utils.url_decode(req.url) + end + end, + }) end ---Register a new autocommand in the `Rest` augroup @@ -104,28 +102,28 @@ end ---@param description string Autocommand description ---@package function autocmds.register_autocmd(events, cb, description) - vim.validate({ - events = { events, "table" }, - cb = { cb, { "function", "string" } }, - description = { description, "string" }, - }) + vim.validate({ + events = { events, "table" }, + cb = { cb, { "function", "string" } }, + description = { description, "string" }, + }) - local autocmd_opts = { - group = vim.api.nvim_create_augroup("Rest", { clear = false }), - desc = description, - } + local autocmd_opts = { + group = vim.api.nvim_create_augroup("Rest", { clear = false }), + desc = description, + } - if type(cb) == "function" then - autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, { - callback = cb, - }) - elseif type(cb) == "string" then - autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, { - command = cb, - }) - end + if type(cb) == "function" then + autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, { + callback = cb, + }) + elseif type(cb) == "string" then + autocmd_opts = vim.tbl_deep_extend("force", autocmd_opts, { + command = cb, + }) + end - vim.api.nvim_create_autocmd(events, autocmd_opts) + vim.api.nvim_create_autocmd(events, autocmd_opts) end return autocmds diff --git a/lua/rest-nvim/client/curl/cli.lua b/lua/rest-nvim/client/curl/cli.lua index 50282bd..e0d30a8 100644 --- a/lua/rest-nvim/client/curl/cli.lua +++ b/lua/rest-nvim/client/curl/cli.lua @@ -18,28 +18,30 @@ local progress = require("fidget.progress") ---@see vim.system ---@param args string[] curl CLI arguments ----@param on_exit fun(sc: vim.SystemCompleted) Called asynchronously when the luarocks command exits. asynchronously. Receives SystemCompleted object, see return of SystemObj:wait(). +---Called asynchronously when the luarocks command exits. +---Receives SystemCompleted object, see return type of SystemObj:wait(). +---@param on_exit fun(sc: vim.SystemCompleted) ---@param opts? vim.SystemOpts ---@package function curl.cli(args, on_exit, opts) - opts = opts or {} - opts.detach = false - opts.text = true - -- TODO(boltless): parse by chunk using `--trace-ascii %` - local curl_cmd = { "curl", "-sL", "-v" } - curl_cmd = vim.list_extend(curl_cmd, args) - log.info(curl_cmd) - opts.detach = false - local ok, e = pcall(vim.system, curl_cmd, opts, on_exit) - if not ok then - ---@type vim.SystemCompleted - local sc = { - code = 99999, - signal = 0, - stderr = "Failed to invoke curl: " .. e, - } - on_exit(sc) - end + opts = opts or {} + opts.detach = false + opts.text = true + -- TODO(boltless): parse by chunk using `--trace-ascii %` + local curl_cmd = { "curl", "-sL", "-v" } + curl_cmd = vim.list_extend(curl_cmd, args) + log.info(curl_cmd) + opts.detach = false + local ok, e = pcall(vim.system, curl_cmd, opts, on_exit) + if not ok then + ---@type vim.SystemCompleted + local sc = { + code = 99999, + signal = 0, + stderr = "Failed to invoke curl: " .. e, + } + on_exit(sc) + end end ---@private @@ -49,12 +51,12 @@ local parser = {} ---@param str string ---@return rest.Response.status function parser.parse_verbose_status(str) - local version, code, text = str:match("^(%S+) (%d+) ?(.*)") - return { - version = version, - code = tonumber(code), - text = text, - } + local version, code, text = str:match("^(%S+) (%d+) ?(.*)") + return { + version = version, + code = tonumber(code), + text = text, + } end ---@package @@ -62,26 +64,26 @@ end ---@return string? key ---@return string? value function parser.parse_header_pair(str) - local key, value = str:match("(%S+):(.*)") - if not key then - return - end - return key:lower(), vim.trim(value) + local key, value = str:match("(%S+):(.*)") + if not key then + return + end + return key:lower(), vim.trim(value) end ---@package ---@param line string ---@return {time:string,prefix:string,str:string?}|nil function parser.parse_verbose_line(line) - local prefix, str = line:match("(.) ?(.*)") - if not prefix then - log.error("Error while parsing verbose curl output:\n" .. line) - return - end - return { - prefix = prefix, - str = str, - } + local prefix, str = line:match("(.) ?(.*)") + if not prefix then + log.error("Error while parsing verbose curl output:\n" .. line) + return + end + return { + prefix = prefix, + str = str, + } end local _VERBOSE_PREFIX_META = "*" @@ -97,49 +99,49 @@ local VERBOSE_PREFIX_STAT = "?" ---@package ---@param str string function parser.parse_stat_pair(str) - local key, value = str:match("(%S+):(.*)") - if not key then - return - end - value = vim.trim(value) - if key:find("size") then - value = utils.transform_size(value) - elseif key:find("time") then - value = utils.transform_time(value) - end - return key, value + local key, value = str:match("(%S+):(.*)") + if not key then + return + end + value = vim.trim(value) + if key:find("size") then + value = utils.transform_size(value) + elseif key:find("time") then + value = utils.transform_time(value) + end + return key, value end ---@param lines string[] ---@return rest.Response function parser.parse_verbose(lines) - local response = { - headers = {}, - statistics = {}, - } - vim.iter(lines):map(parser.parse_verbose_line):each(function(ln) - if ln.prefix == VERBOSE_PREFIX_RES_HEADER then - if not response.status then - -- response status - response.status = parser.parse_verbose_status(ln.str) - else - -- response header - local key, value = parser.parse_header_pair(ln.str) - if key then - if not response.headers[key] then - response.headers[key] = {} - end - table.insert(response.headers[key], value) + local response = { + headers = {}, + statistics = {}, + } + vim.iter(lines):map(parser.parse_verbose_line):each(function(ln) + if ln.prefix == VERBOSE_PREFIX_RES_HEADER then + if not response.status then + -- response status + response.status = parser.parse_verbose_status(ln.str) + else + -- response header + local key, value = parser.parse_header_pair(ln.str) + if key then + if not response.headers[key] then + response.headers[key] = {} + end + table.insert(response.headers[key], value) + end + end + elseif ln.prefix == VERBOSE_PREFIX_STAT then + local key, value = parser.parse_stat_pair(ln.str) + if key then + response.statistics[key] = value + end end - end - elseif ln.prefix == VERBOSE_PREFIX_STAT then - local key, value = parser.parse_stat_pair(ln.str) - if key then - response.statistics[key] = value - end - end - end) - return response + end) + return response end --- Builder --- @@ -147,12 +149,12 @@ end ---@param kv table ---@return string[] local function kv_to_list(kv, prefix, sep) - local tbl = {} - for key, value in pairs(kv) do - table.insert(tbl, prefix) - table.insert(tbl, key .. sep .. value) - end - return tbl + local tbl = {} + for key, value in pairs(kv) do + table.insert(tbl, prefix) + table.insert(tbl, key .. sep .. value) + end + return tbl end ---@private @@ -161,96 +163,97 @@ local builder = {} ---@param method string ---@return string[] args function builder.method(method) - if method ~= "head" then - return { "-X", string.upper(method) } - else - return { "-I" } - end + if method ~= "head" then + return { "-X", string.upper(method) } + else + return { "-I" } + end end ---@package ---@param header table ---@return string[] args function builder.headers(header) - local args = {} - local upper = function(str) - return string.gsub(" " .. str, "%W%l", string.upper):sub(2) - end - for key, values in pairs(header) do - for _, value in ipairs(values) do - vim.list_extend(args, { "-H", upper(key) .. ": " .. value }) + local args = {} + local upper = function(str) + return string.gsub(" " .. str, "%W%l", string.upper):sub(2) + end + for key, values in pairs(header) do + for _, value in ipairs(values) do + vim.list_extend(args, { "-H", upper(key) .. ": " .. value }) + end end - end - return args + return args end ---@param cookies rest.Cookie[] ---@return string[] args function builder.cookies(cookies) - return vim.iter(cookies):map(function (cookie) - return { "-b", cookie.name .. "=" .. cookie.value } - end):totable() + return vim.iter(cookies) + :map(function(cookie) + return { "-b", cookie.name .. "=" .. cookie.value } + end) + :totable() end ---@param body string? ---@return string[]? args function builder.raw_body(body) - if not body then - return - end - return { "--data-raw", body } + if not body then + return + end + return { "--data-raw", body } end ---@package ---@param body table? ---@return string[]? args function builder.data_body(body) - if not body then - return - end - return kv_to_list(body, "-d", "=") + if not body then + return + end + return kv_to_list(body, "-d", "=") end function builder.file(file) - if not file then - return - end - -- FIXME: should normalize/expand the file path - return { "--data-binary", "@" .. file } + if not file then + return + end + -- FIXME: should normalize/expand the file path + return { "--data-binary", "@" .. file } end ---@package ---@param version string ---@return string[]? args function builder.http_version(version) - vim.validate({ - version = { - version, - function(v) - return not v or vim.list_contains({ "HTTP/0.9", "HTTP/1.0", "HTTP/1.1", "HTTP/2", "HTTP/3" }, v) - end, - }, - }) - if not version then - return - end - return { "--" .. version:lower():gsub("/", "") } + vim.validate({ + version = { + version, + function(v) + return not v or vim.list_contains({ "HTTP/0.9", "HTTP/1.0", "HTTP/1.1", "HTTP/2", "HTTP/3" }, v) + end, + }, + }) + if not version then + return + end + return { "--" .. version:lower():gsub("/", "") } end ---@return string[]? args function builder.statistics() - if vim.tbl_isempty(config.clients.curl.statistics) then - return - end - local args = { "-w" } - local format = vim - .iter(config.clients.curl.statistics) - :map(function(key, _style) - return ("? %s:%%{%s}\n"):format(key, key) - end) - :join("") - table.insert(args, "%{stderr}" .. format) - return args + if vim.tbl_isempty(config.clients.curl.statistics) then + return + end + local args = { "-w" } + local format = vim.iter(config.clients.curl.statistics) + :map(function(key, _style) + return ("? %s:%%{%s}\n"):format(key, key) + end) + :join("") + table.insert(args, "%{stderr}" .. format) + return args end ---@package @@ -261,90 +264,91 @@ builder.STAT_ARGS = builder.statistics() ---@param ignore_stats? boolean ---@return string[] args function builder.build(req, ignore_stats) - local args = {} - ---@param list table - ---@param value any - local function insert(list, value) - if value then - table.insert(list, value) + local args = {} + ---@param list table + ---@param value any + local function insert(list, value) + if value then + table.insert(list, value) + end end - end - insert(args, req.url) - insert(args, builder.method(req.method)) - insert(args, builder.headers(req.headers)) - insert(args, builder.cookies(req.cookies)) - if req.body then - if req.body.__TYPE == "external" then - insert(args, builder.file(req.body.data.path)) - elseif req.body.__TYPE == "graphql" then - log.error("graqphql body is not supportted yet") - elseif req.body.__TYPE == "multipart_form_data" then - log.error("multipart-form-data body is not supportted yet") - elseif vim.list_contains({ "json", "xml", "raw" }, req.body.__TYPE) then - insert(args, builder.raw_body(req.body.data)) - else - log.error(("unkown body type: '%s'"):format(req.body.__TYPE)) + insert(args, req.url) + insert(args, builder.method(req.method)) + insert(args, builder.headers(req.headers)) + insert(args, builder.cookies(req.cookies)) + if req.body then + if req.body.__TYPE == "external" then + insert(args, builder.file(req.body.data.path)) + elseif req.body.__TYPE == "graphql" then + log.error("graqphql body is not supportted yet") + elseif req.body.__TYPE == "multipart_form_data" then + log.error("multipart-form-data body is not supportted yet") + elseif vim.list_contains({ "json", "xml", "raw" }, req.body.__TYPE) then + insert(args, builder.raw_body(req.body.data)) + else + log.error(("unkown body type: '%s'"):format(req.body.__TYPE)) + end + end + if config.request.skip_ssl_verification then + insert(args, "-k") end - end - if config.request.skip_ssl_verification then - insert(args, "-k") - end - -- TODO: auth? - insert(args, builder.http_version(req.http_version) or {}) - if not ignore_stats then - insert(args, builder.STAT_ARGS) - end - return vim.iter(args):flatten(math.huge):totable() + -- TODO: auth? + insert(args, builder.http_version(req.http_version) or {}) + if not ignore_stats then + insert(args, builder.STAT_ARGS) + end + return vim.iter(args):flatten(math.huge):totable() end ---Generate curl command equivelant to given request. ---This command doesn't include verbose/trace options ---@param req rest.Request function builder.build_command(req) - local base_cmd = "curl -sL" - local args = vim.iter(builder.build(req, true)):map(function (a) - return vim.fn.shellescape(a) - end) - return base_cmd .. " " .. args:join(" ") + local base_cmd = "curl -sL" + local args = vim.iter(builder.build(req, true)):map(function(a) + return vim.fn.shellescape(a) + end) + return base_cmd .. " " .. args:join(" ") end ---Send request via `curl` cli ---@param request rest.Request Request data to be passed to cURL ---@return nio.control.Future future Future containing rest.Response function curl.request(request) - local progress_handle = progress.handle.create({ - title = "Executing", - message = "Executing request...", - lsp_client = { name = "rest.nvim" }, - }) - local future = nio.control.future() - local args = builder.build(request) - curl.cli(args, function(sc) - if sc.code ~= 0 then - local message = "Something went wrong when making the request with cURL:\n" .. curl_utils.curl_error(sc.code) - progress_handle:cancel() - log.error(message) - future.set_error(message) - return - end - vim.schedule(function () - progress_handle:report({ - message = "Parsing response...", - }) - local response = parser.parse_verbose(vim.split(sc.stderr, "\n")) - response.body = sc.stdout - future.set(response) - progress_handle:report({ - message = "Success", - }) - progress_handle:finish() - end) - end, { - -- TODO(boltless): parse by chunk from here - -- stdout = function (err, chunk) end, - -- stderr = function (err, chunk) end, - }) - return future + local progress_handle = progress.handle.create({ + title = "Executing", + message = "Executing request...", + lsp_client = { name = "rest.nvim" }, + }) + local future = nio.control.future() + local args = builder.build(request) + curl.cli(args, function(sc) + if sc.code ~= 0 then + local message = "Something went wrong when making the request with cURL:\n" + .. curl_utils.curl_error(sc.code) + progress_handle:cancel() + log.error(message) + future.set_error(message) + return + end + vim.schedule(function() + progress_handle:report({ + message = "Parsing response...", + }) + local response = parser.parse_verbose(vim.split(sc.stderr, "\n")) + response.body = sc.stdout + future.set(response) + progress_handle:report({ + message = "Success", + }) + progress_handle:finish() + end) + end, { + -- TODO(boltless): parse by chunk from here + -- stdout = function (err, chunk) end, + -- stderr = function (err, chunk) end, + }) + return future end curl.builder = builder diff --git a/lua/rest-nvim/client/curl/libcurl.lua b/lua/rest-nvim/client/curl/libcurl.lua index 0ab4d49..767da14 100644 --- a/lua/rest-nvim/client/curl/libcurl.lua +++ b/lua/rest-nvim/client/curl/libcurl.lua @@ -23,32 +23,34 @@ local curl_cli = require("rest-nvim.client.curl.cli") ---@param statistics_tbl table Statistics table ---@return table stats Request statistics local function get_stats(req, statistics_tbl) - local function get_stat(req_, stat_) - local curl_info = curl["INFO_" .. stat_:upper()] - if not curl_info then - logger.error( - "The cURL request stat field '" - .. stat_("' was not found.\nPlease take a look at: https://curl.se/libcurl/c/curl_easy_getinfo.html") - ) - return + local function get_stat(req_, stat_) + local curl_info = curl["INFO_" .. stat_:upper()] + if not curl_info then + logger.error( + "The cURL request stat field '" + .. stat_( + "' was not found.\nPlease take a look at: https://curl.se/libcurl/c/curl_easy_getinfo.html" + ) + ) + return + end + local stat_info = req_:getinfo(curl_info) + + if stat_:find("size") then + stat_info = utils.transform_size(stat_info) + elseif stat_:find("time") then + stat_info = utils.transform_time(stat_info) + end + return stat_info end - local stat_info = req_:getinfo(curl_info) - if stat_:find("size") then - stat_info = utils.transform_size(stat_info) - elseif stat_:find("time") then - stat_info = utils.transform_time(stat_info) - end - return stat_info - end - - local stats = {} + local stats = {} - for name, _ in pairs(statistics_tbl) do - stats[name] = get_stat(req, name) - end + for name, _ in pairs(statistics_tbl) do + stats[name] = get_stat(req, name) + end - return stats + return stats end ---Execute an HTTP request using cURL @@ -56,134 +58,134 @@ end ---@param req rest.Request Request data to be passed to cURL ---@return rest.Response? info The request information (url, method, headers, body, etc) function client.request(req) - logger.info("sending request to: " .. req.url) - if not found_curl then - ---@diagnostic disable-next-line need-check-nil - logger.error("lua-curl could not be found, therefore the cURL client will not work.") - return - end - local host = req.headers["host"] - if host then - req.url = host .. req.url - end - - -- We have to concat request headers to a single string, e.g. ["Content-Type"]: "application/json" -> "Content-Type: application/json" - local headers = {} - for name, values in pairs(req.headers) do - for _, value in ipairs(values) do - table.insert(headers, name .. ": " .. value) + logger.info("sending request to: " .. req.url) + if not found_curl then + ---@diagnostic disable-next-line need-check-nil + logger.error("lua-curl could not be found, therefore the cURL client will not work.") + return + end + local host = req.headers["host"] + if host then + req.url = host .. req.url end - end - - -- Whether to skip SSL host and peer verification - local skip_ssl_verification = config.skip_ssl_verification - local req_ = curl.easy_init() - req_:setopt({ - url = req.url, - -- verbose = true, - httpheader = headers, - ssl_verifyhost = skip_ssl_verification, - ssl_verifypeer = skip_ssl_verification, - }) - - -- Encode URL query parameters and set the request URL again with the encoded values - local should_encode_url = config.encode_url - if should_encode_url then - -- Create a new URL as we cannot extract the URL from the req object - local url_ = curl.url() - url_:set_url(req.url) - -- Re-add the request query with the encoded parameters - local query = url_:get_query() - if type(query) == "string" then - url_:set_query("") - for param in vim.gsplit(query, "&") do - url_:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY) - end + + -- We have to concat request headers to a single string, e.g. ["Content-Type"]: "application/json" -> "Content-Type: application/json" + local headers = {} + for name, values in pairs(req.headers) do + for _, value in ipairs(values) do + table.insert(headers, name .. ": " .. value) + end + end + + -- Whether to skip SSL host and peer verification + local skip_ssl_verification = config.skip_ssl_verification + local req_ = curl.easy_init() + req_:setopt({ + url = req.url, + -- verbose = true, + httpheader = headers, + ssl_verifyhost = skip_ssl_verification, + ssl_verifypeer = skip_ssl_verification, + }) + + -- Encode URL query parameters and set the request URL again with the encoded values + local should_encode_url = config.encode_url + if should_encode_url then + -- Create a new URL as we cannot extract the URL from the req object + local url_ = curl.url() + url_:set_url(req.url) + -- Re-add the request query with the encoded parameters + local query = url_:get_query() + if type(query) == "string" then + url_:set_query("") + for param in vim.gsplit(query, "&") do + url_:set_query(param, curl.U_URLENCODE + curl.U_APPENDQUERY) + end + end + -- Re-add the request URL to the req object + req_:setopt_url(url_:get_url()) end - -- Re-add the request URL to the req object - req_:setopt_url(url_:get_url()) - end - - -- Set request HTTP version, defaults to HTTP/1.1 - if req.http_version then - local http_version = req.http_version:gsub("%.", "_") - req_:setopt_http_version(curl["HTTP_VERSION_" .. http_version]) - else - req_:setopt_http_version(curl.HTTP_VERSION_1_1) - end - - -- If the request method is not GET then we have to build the method in our own - -- See: https://github.com/Lua-cURL/Lua-cURLv3/issues/156 - local method = req.method - if vim.tbl_contains({ "POST", "PUT", "PATCH", "TRACE", "OPTIONS", "DELETE" }, method) then - req_:setopt_post(true) - req_:setopt_customrequest(method) - end - - -- local body = vim.deepcopy(request.body) - if req.body then - if req.body.__TYPE == "json" then - req_:setopt_postfields(req.body.data) - elseif req.body.__TYPE == "xml" then - req_:setopt_postfields(req.body.data) - elseif req.body.__TYPE == "external" then - local mimetypes = require("mimetypes") - local body_mimetype = mimetypes.guess(req.body.data.path) - local post_data = { - [req.body.data.name and req.body.data.name or "body"] = { - file = req.body.data.path, - type = body_mimetype, - }, - } - req_:post(post_data) - elseif req.body.__TYPE == "form" then - local form = curl.form() - for k, v in pairs(req.body.data) do - form:add_content(k, v) - end - req_:setopt_httppost(form) + + -- Set request HTTP version, defaults to HTTP/1.1 + if req.http_version then + local http_version = req.http_version:gsub("%.", "_") + req_:setopt_http_version(curl["HTTP_VERSION_" .. http_version]) else - logger.error(("'%s' type body is not supported yet"):format(req.body.__TYPE)) - return + req_:setopt_http_version(curl.HTTP_VERSION_1_1) + end + + -- If the request method is not GET then we have to build the method in our own + -- See: https://github.com/Lua-cURL/Lua-cURLv3/issues/156 + local method = req.method + if vim.tbl_contains({ "POST", "PUT", "PATCH", "TRACE", "OPTIONS", "DELETE" }, method) then + req_:setopt_post(true) + req_:setopt_customrequest(method) + end + + -- local body = vim.deepcopy(request.body) + if req.body then + if req.body.__TYPE == "json" then + req_:setopt_postfields(req.body.data) + elseif req.body.__TYPE == "xml" then + req_:setopt_postfields(req.body.data) + elseif req.body.__TYPE == "external" then + local mimetypes = require("mimetypes") + local body_mimetype = mimetypes.guess(req.body.data.path) + local post_data = { + [req.body.data.name and req.body.data.name or "body"] = { + file = req.body.data.path, + type = body_mimetype, + }, + } + req_:post(post_data) + elseif req.body.__TYPE == "form" then + local form = curl.form() + for k, v in pairs(req.body.data) do + form:add_content(k, v) + end + req_:setopt_httppost(form) + else + logger.error(("'%s' type body is not supported yet"):format(req.body.__TYPE)) + return + end + end + + -- Request execution + local res_result = {} + ---@type table + local res_raw_headers = {} + req_:setopt_writefunction(table.insert, res_result) + req_:setopt_headerfunction(table.insert, res_raw_headers) + + local ok, err = req_:perform() + if not ok then + logger.error("Something went wrong when making the request with cURL:\n" .. curl_utils.curl_error(err:no())) + return end - end - - -- Request execution - local res_result = {} - ---@type table - local res_raw_headers = {} - req_:setopt_writefunction(table.insert, res_result) - req_:setopt_headerfunction(table.insert, res_raw_headers) - - local ok, err = req_:perform() - if not ok then - logger.error("Something went wrong when making the request with cURL:\n" .. curl_utils.curl_error(err:no())) - return - end - ---@diagnostic disable-next-line: invisible - local status = curl_cli.parser.parse_verbose_status(table.remove(res_raw_headers, 1)) - local res_headers = {} - for _, header in ipairs(res_raw_headers) do ---@diagnostic disable-next-line: invisible - local key, value = curl_cli.parser.parse_header_pair(header) - if key then - if not res_headers[key] then - res_headers[key] = {} - end - table.insert(res_headers[key], value) + local status = curl_cli.parser.parse_verbose_status(table.remove(res_raw_headers, 1)) + local res_headers = {} + for _, header in ipairs(res_raw_headers) do + ---@diagnostic disable-next-line: invisible + local key, value = curl_cli.parser.parse_header_pair(header) + if key then + if not res_headers[key] then + res_headers[key] = {} + end + table.insert(res_headers[key], value) + end end - end - ---@type rest.Response - local res = { - status = status, - headers = res_headers, - body = table.concat(res_result), - statistics = get_stats(req_, {}) - } - logger.debug(vim.inspect(res.headers)) - res.status.text = vim.trim(res.status.text) - req_:close() - return res + ---@type rest.Response + local res = { + status = status, + headers = res_headers, + body = table.concat(res_result), + statistics = get_stats(req_, {}), + } + logger.debug(vim.inspect(res.headers)) + res.status.text = vim.trim(res.status.text) + req_:close() + return res end return client diff --git a/lua/rest-nvim/client/curl/utils.lua b/lua/rest-nvim/client/curl/utils.lua index 60f6088..f5ba41e 100644 --- a/lua/rest-nvim/client/curl/utils.lua +++ b/lua/rest-nvim/client/curl/utils.lua @@ -5,92 +5,92 @@ local utils = {} ---@param code number The exit code of curl ---@return string The curl error message function utils.curl_error(code) - local curl_error_dictionary = { - [1] = "Unsupported protocol. This build of curl has no support for this protocol.", - [2] = "Failed to initialize.", - [3] = "URL malformed. The syntax was not correct.", - [4] = "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time." - .. "To make curl able to do this, you probably need another build of libcurl!", - [5] = "Couldn't resolve proxy. The given proxy host could not be resolved.", - [6] = "Couldn't resolve host. The given remote host was not resolved.", - [7] = "Failed to connect to host.", - [8] = "Weird server reply. The server sent data curl couldn't parse.", - [9] = "FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn't exist on the server.", - [10] = "FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar.", - [11] = "FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request.", - [12] = "During an active FTP session while waiting for the server to connect back to curl, the timeout expired.", - [13] = "FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request.", - [14] = "FTP weird 227 format. Curl couldn't parse the 227-line the server sent.", - [15] = "FTP can't get host. Couldn't resolve the host IP we got in the 227-line.", - [16] = "HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details.", - [17] = "FTP couldn't set binary. Couldn't change transfer method to binary.", - [18] = "Partial file. Only a part of the file was transferred.", - [19] = "FTP couldn't download/access the given file, the RETR (or similar) command failed.", - [21] = "FTP quote error. A quote command returned error from the server.", - [22] = "HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used.", - [23] = "Write error. Curl couldn't write data to a local filesystem or similar.", - [25] = "FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading.", - [26] = "Read error. Various reading problems.", - [27] = "Out of memory. A memory allocation request failed.", - [28] = "Operation timeout. The specified time-out period was reached according to the conditions.", - [30] = "FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!", - [31] = "FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers.", - [33] = 'HTTP range error. The range "command" didn\'t work.', - [34] = "HTTP post error. Internal post-request generation error.", - [35] = "SSL connect error. The SSL handshaking failed.", - [36] = "Bad download resume. Couldn't continue an earlier aborted download.", - [37] = "FILE couldn't read file. Failed to open the file. Permissions?", - [38] = "LDAP cannot bind. LDAP bind operation failed.", - [39] = "LDAP search failed.", - [41] = "Function not found. A required LDAP function was not found.", - [42] = "Aborted by callback. An application told curl to abort the operation.", - [43] = "Internal error. A function was called with a bad parameter.", - [45] = "Interface error. A specified outgoing interface could not be used.", - [47] = "Too many redirects. When following redirects, curl hit the maximum amount.", - [48] = "Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!", - [49] = "Malformed telnet option.", - [51] = "The peer's SSL certificate or SSH MD5 fingerprint was not OK.", - [52] = "The server didn't reply anything, which here is considered an error.", - [53] = "SSL crypto engine not found.", - [54] = "Cannot set SSL crypto engine as default.", - [55] = "Failed sending network data.", - [56] = "Failure in receiving network data.", - [58] = "Problem with the local certificate.", - [59] = "Couldn't use specified SSL cipher.", - [60] = "Peer certificate cannot be authenticated with known CA certificates.", - [61] = "Unrecognized transfer encoding.", - [62] = "Invalid LDAP URL.", - [63] = "Maximum file size exceeded.", - [64] = "Requested FTP SSL level failed.", - [65] = "Sending the data requires a rewind that failed.", - [66] = "Failed to initialize SSL Engine.", - [67] = "The user name, password, or similar was not accepted and curl failed to log in.", - [68] = "File not found on TFTP server.", - [69] = "Permission problem on TFTP server.", - [70] = "Out of disk space on TFTP server.", - [71] = "Illegal TFTP operation.", - [72] = "Unknown TFTP transfer ID.", - [73] = "File already exists (TFTP).", - [74] = "No such user (TFTP).", - [75] = "Character conversion failed.", - [76] = "Character conversion functions required.", - [77] = "Problem with reading the SSL CA cert (path? access rights?).", - [78] = "The resource referenced in the URL does not exist.", - [79] = "An unspecified error occurred during the SSH session.", - [80] = "Failed to shut down the SSL connection.", - [82] = "Could not load CRL file, missing or wrong format (added in 7.19.0).", - [83] = "Issuer check failed (added in 7.19.0).", - [84] = "The FTP PRET command failed", - [85] = "RTSP: mismatch of CSeq numbers", - [86] = "RTSP: mismatch of Session Identifiers", - [87] = "unable to parse FTP file list", - [88] = "FTP chunk callback reported error", - [89] = "No connection available, the session will be queued", - [90] = "SSL public key does not matched pinned public key", - [91] = "Invalid SSL certificate status.", - [92] = "Stream error in HTTP/2 framing layer.", - } - return ("cURL error (%d): %s"):format(code, curl_error_dictionary[code] or "Unknown curl error") + local curl_error_dictionary = { + [1] = "Unsupported protocol. This build of curl has no support for this protocol.", + [2] = "Failed to initialize.", + [3] = "URL malformed. The syntax was not correct.", + [4] = "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time." + .. "To make curl able to do this, you probably need another build of libcurl!", + [5] = "Couldn't resolve proxy. The given proxy host could not be resolved.", + [6] = "Couldn't resolve host. The given remote host was not resolved.", + [7] = "Failed to connect to host.", + [8] = "Weird server reply. The server sent data curl couldn't parse.", + [9] = "FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn't exist on the server.", + [10] = "FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar.", + [11] = "FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request.", + [12] = "During an active FTP session while waiting for the server to connect back to curl, the timeout expired.", + [13] = "FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request.", + [14] = "FTP weird 227 format. Curl couldn't parse the 227-line the server sent.", + [15] = "FTP can't get host. Couldn't resolve the host IP we got in the 227-line.", + [16] = "HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details.", + [17] = "FTP couldn't set binary. Couldn't change transfer method to binary.", + [18] = "Partial file. Only a part of the file was transferred.", + [19] = "FTP couldn't download/access the given file, the RETR (or similar) command failed.", + [21] = "FTP quote error. A quote command returned error from the server.", + [22] = "HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used.", + [23] = "Write error. Curl couldn't write data to a local filesystem or similar.", + [25] = "FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading.", + [26] = "Read error. Various reading problems.", + [27] = "Out of memory. A memory allocation request failed.", + [28] = "Operation timeout. The specified time-out period was reached according to the conditions.", + [30] = "FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!", + [31] = "FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers.", + [33] = 'HTTP range error. The range "command" didn\'t work.', + [34] = "HTTP post error. Internal post-request generation error.", + [35] = "SSL connect error. The SSL handshaking failed.", + [36] = "Bad download resume. Couldn't continue an earlier aborted download.", + [37] = "FILE couldn't read file. Failed to open the file. Permissions?", + [38] = "LDAP cannot bind. LDAP bind operation failed.", + [39] = "LDAP search failed.", + [41] = "Function not found. A required LDAP function was not found.", + [42] = "Aborted by callback. An application told curl to abort the operation.", + [43] = "Internal error. A function was called with a bad parameter.", + [45] = "Interface error. A specified outgoing interface could not be used.", + [47] = "Too many redirects. When following redirects, curl hit the maximum amount.", + [48] = "Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!", + [49] = "Malformed telnet option.", + [51] = "The peer's SSL certificate or SSH MD5 fingerprint was not OK.", + [52] = "The server didn't reply anything, which here is considered an error.", + [53] = "SSL crypto engine not found.", + [54] = "Cannot set SSL crypto engine as default.", + [55] = "Failed sending network data.", + [56] = "Failure in receiving network data.", + [58] = "Problem with the local certificate.", + [59] = "Couldn't use specified SSL cipher.", + [60] = "Peer certificate cannot be authenticated with known CA certificates.", + [61] = "Unrecognized transfer encoding.", + [62] = "Invalid LDAP URL.", + [63] = "Maximum file size exceeded.", + [64] = "Requested FTP SSL level failed.", + [65] = "Sending the data requires a rewind that failed.", + [66] = "Failed to initialize SSL Engine.", + [67] = "The user name, password, or similar was not accepted and curl failed to log in.", + [68] = "File not found on TFTP server.", + [69] = "Permission problem on TFTP server.", + [70] = "Out of disk space on TFTP server.", + [71] = "Illegal TFTP operation.", + [72] = "Unknown TFTP transfer ID.", + [73] = "File already exists (TFTP).", + [74] = "No such user (TFTP).", + [75] = "Character conversion failed.", + [76] = "Character conversion functions required.", + [77] = "Problem with reading the SSL CA cert (path? access rights?).", + [78] = "The resource referenced in the URL does not exist.", + [79] = "An unspecified error occurred during the SSH session.", + [80] = "Failed to shut down the SSL connection.", + [82] = "Could not load CRL file, missing or wrong format (added in 7.19.0).", + [83] = "Issuer check failed (added in 7.19.0).", + [84] = "The FTP PRET command failed", + [85] = "RTSP: mismatch of CSeq numbers", + [86] = "RTSP: mismatch of Session Identifiers", + [87] = "unable to parse FTP file list", + [88] = "FTP chunk callback reported error", + [89] = "No connection available, the session will be queued", + [90] = "SSL public key does not matched pinned public key", + [91] = "Invalid SSL certificate status.", + [92] = "Stream error in HTTP/2 framing layer.", + } + return ("cURL error (%d): %s"):format(code, curl_error_dictionary[code] or "Unknown curl error") end return utils diff --git a/lua/rest-nvim/client/curl_cli.lua b/lua/rest-nvim/client/curl_cli.lua index 63a151c..3efa2e7 100644 --- a/lua/rest-nvim/client/curl_cli.lua +++ b/lua/rest-nvim/client/curl_cli.lua @@ -1,28 +1,28 @@ local curl_cli = require("rest-nvim.client.curl.cli") local COMPATIBLE_METHODS = { - "OPTIONS", - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "TRACE", - "CONNECT", - "PATCH", - "LIST", + "OPTIONS", + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE", + "CONNECT", + "PATCH", + "LIST", } ---@type rest.Client local client = { - name = "curl_cli", - request = curl_cli.request, - available = function (req) - local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method) - local scheme = req.url:match("^(.+)://") - local scheme_ok = (not scheme) or scheme == "http" or scheme == "https" - return method_ok and scheme_ok - end + name = "curl_cli", + request = curl_cli.request, + available = function(req) + local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method) + local scheme = req.url:match("^(.+)://") + local scheme_ok = (not scheme) or scheme == "http" or scheme == "https" + return method_ok and scheme_ok + end, } return client diff --git a/lua/rest-nvim/client/init.lua b/lua/rest-nvim/client/init.lua index df1e449..e614a63 100644 --- a/lua/rest-nvim/client/init.lua +++ b/lua/rest-nvim/client/init.lua @@ -11,30 +11,30 @@ local clients = {} ---@field available fun(req: rest.Request):boolean clients.clients = { - require("rest-nvim.client.curl_cli"), - -- require("rest-nvim.client.libcurl"), + require("rest-nvim.client.curl_cli"), + -- require("rest-nvim.client.libcurl"), } function clients.register_client(client) - vim.validate({ - client = { - client, - function (c) - return type(c) == "table" and type(c.request) == "function" and type(c.available) == "function" - end, - "table with `name`, `request()` and `available()` fields" - } - }) - table.insert(clients.clients, client) + vim.validate({ + client = { + client, + function(c) + return type(c) == "table" and type(c.request) == "function" and type(c.available) == "function" + end, + "table with `name`, `request()` and `available()` fields", + }, + }) + table.insert(clients.clients, client) end ---Find all registered clients available for given request ---@param req rest.Request ---@return rest.Client[] function clients.get_available_clients(req) - return vim.tbl_filter(function (c) - return c.available(req) - end, clients.clients) + return vim.tbl_filter(function(c) + return c.available(req) + end, clients.clients) end return clients diff --git a/lua/rest-nvim/client/libcurl.lua b/lua/rest-nvim/client/libcurl.lua index 1f998a2..83e4f8b 100644 --- a/lua/rest-nvim/client/libcurl.lua +++ b/lua/rest-nvim/client/libcurl.lua @@ -3,40 +3,40 @@ local libcurl = require("rest-nvim.client.curl.libcurl") local nio = require("nio") local COMPATIBLE_METHODS = { - "OPTIONS", - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "TRACE", - "CONNECT", - "PATCH", - "LIST", + "OPTIONS", + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE", + "CONNECT", + "PATCH", + "LIST", } local COMPATIBLE_BODY_TYPES = { - "json", - "xml", - "external", - "form", + "json", + "xml", + "external", + "form", } ---@type rest.Client local client = { - name = "libcurl", - request = function (req) - local res = libcurl.request(req) - local future = nio.control.future() - future.set(res) - return future - end, - available = function (req) - local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method) - local url_ok = req.url:match("^https?://") - local body_ok = (not req.body) or vim.list_contains(COMPATIBLE_BODY_TYPES, req.body.__TYPE) - return method_ok and url_ok and body_ok - end + name = "libcurl", + request = function(req) + local res = libcurl.request(req) + local future = nio.control.future() + future.set(res) + return future + end, + available = function(req) + local method_ok = vim.list_contains(COMPATIBLE_METHODS, req.method) + local url_ok = req.url:match("^https?://") + local body_ok = (not req.body) or vim.list_contains(COMPATIBLE_BODY_TYPES, req.body.__TYPE) + return method_ok and url_ok and body_ok + end, } return client diff --git a/lua/rest-nvim/commands.lua b/lua/rest-nvim/commands.lua index 4634ba6..fbcd8d0 100644 --- a/lua/rest-nvim/commands.lua +++ b/lua/rest-nvim/commands.lua @@ -66,229 +66,280 @@ local function config() return require("rest-nvim.config") end ---@param opts table ---@return integer winnr local function split_open_cmd(opts) - local is_split = opts.smods.vertical or opts.smods.horizontal - local is_tab = opts.smods.tab ~= -1 - if is_split or is_tab then - vim.cmd(opts.mods .. " split") - end - return vim.api.nvim_get_current_win() + local is_split = opts.smods.vertical or opts.smods.horizontal + local is_tab = opts.smods.tab ~= -1 + if is_split or is_tab then + vim.cmd(opts.mods .. " split") + end + return vim.api.nvim_get_current_win() end ---@type table local rest_command_tbl = { - open = { - impl = function (_, opts) - local winnr = split_open_cmd(opts) - ui().enter(winnr) - end - }, - run = { - impl = function(args, _) - if vim.bo.filetype ~= "http" or vim.b.__rest_no_http_file then - vim.notify("`:Rest run` can be only called from http file", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - if #args > 1 then - vim.notify("Running multiple request isn't supported yet", vim.log.levels.WARN, { title = "rest.nvim" }) - return - elseif #args == 1 then - request().run_by_name(args[1]) - return - end - ui().clear() - if not ui().is_open() then - vim.cmd.wincmd("v") - ui().enter(0) - vim.cmd.wincmd("p") - end - request().run() - end, - ---@return string[] - complete = function (args) - local names = parser().get_request_names(0) - local matches = vim.iter(names):filter(function (name) - return name:find("^" .. vim.pesc(args)) - end):map(function (name) - name = name:gsub("%s+", "\\ ") - return name - end):totable() - return matches - end - }, - last = { - impl = function(_, _) - request().run_last() - end, - }, - logs = { - impl = function(_, opts) - local winnr = split_open_cmd(opts) - vim.api.nvim_win_call(winnr, function () - ---@diagnostic disable-next-line: invisible - vim.cmd.edit(logger().get_logfile()) - end) - end, - }, - cookies = { - impl = function(_, opts) - local winnr = split_open_cmd(opts) - vim.api.nvim_win_call(winnr, function () - vim.cmd.edit(config().cookies.path) - end) - end, - }, - env = { - impl = function(args, _) - if not args[1] or args[1] == "show" then - dotenv().show_registered_file() - return - elseif args[1] == "set" then - if #args < 2 then - vim.notify("Not enough arguments were passed to the 'env' command: 2 argument were expected, 1 was passed", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - dotenv().register_file(args[2]) - elseif args[1] == "select" then - dotenv().select_file() - else - vim.notify("Invalid action '" .. args[1] .. "' provided to 'env' command", vim.log.levels.ERROR, { title = "rest.nvim" }) - end - end, - ---@return string[] - complete = function(args) - local actions = { "show", "set", "select" } - if #args < 1 then - return actions - end + open = { + impl = function(_, opts) + local winnr = split_open_cmd(opts) + ui().enter(winnr) + end, + }, + run = { + impl = function(args, _) + if vim.bo.filetype ~= "http" or vim.b.__rest_no_http_file then + vim.notify( + "`:Rest run` can be only called from http file", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + if #args > 1 then + vim.notify("Running multiple request isn't supported yet", vim.log.levels.WARN, { title = "rest.nvim" }) + return + elseif #args == 1 then + request().run_by_name(args[1]) + return + end + ui().clear() + if not ui().is_open() then + vim.cmd.wincmd("v") + ui().enter(0) + vim.cmd.wincmd("p") + end + request().run() + end, + ---@return string[] + complete = function(args) + local names = parser().get_request_names(0) + local matches = vim.iter(names) + :filter(function(name) + return name:find("^" .. vim.pesc(args)) + end) + :map(function(name) + name = name:gsub("%s+", "\\ ") + return name + end) + :totable() + return matches + end, + }, + last = { + impl = function(_, _) + request().run_last() + end, + }, + logs = { + impl = function(_, opts) + local winnr = split_open_cmd(opts) + vim.api.nvim_win_call(winnr, function() + ---@diagnostic disable-next-line: invisible + vim.cmd.edit(logger().get_logfile()) + end) + end, + }, + cookies = { + impl = function(_, opts) + local winnr = split_open_cmd(opts) + vim.api.nvim_win_call(winnr, function() + vim.cmd.edit(config().cookies.path) + end) + end, + }, + env = { + impl = function(args, _) + if not args[1] or args[1] == "show" then + dotenv().show_registered_file() + return + elseif args[1] == "set" then + if #args < 2 then + vim.notify( + "Not enough arguments were passed to the 'env' command: 2 argument were expected, 1 was passed", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + dotenv().register_file(args[2]) + elseif args[1] == "select" then + dotenv().select_file() + else + vim.notify( + "Invalid action '" .. args[1] .. "' provided to 'env' command", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + end + end, + ---@return string[] + complete = function(args) + local actions = { "show", "set", "select" } + if #args < 1 then + return actions + end - -- If the completion arguments have a whitespace then treat them as a table instead for easiness - if args:find(" ") then - args = vim.split(args, " ", { trimempty = true }) - end - -- If the completion arguments is a table and `set` is the desired action then - -- return a list of files in the current working directory for completion - if type(args) == "table" and args[1]:match("set") then - return dotenv().find_env_files() - end + -- If the completion arguments have a whitespace then treat them as a table instead for easiness + if args:find(" ") then + args = vim.split(args, " ", { trimempty = true }) + end + -- If the completion arguments is a table and `set` is the desired action then + -- return a list of files in the current working directory for completion + if type(args) == "table" and args[1]:match("set") then + return dotenv().find_env_files() + end - local match = vim.tbl_filter(function(action) - if string.find(action, "^" .. args) then - return action - ---@diagnostic disable-next-line missing-return - end - end, actions) + local match = vim.tbl_filter(function(action) + if string.find(action, "^" .. args) then + return action + ---@diagnostic disable-next-line missing-return + end + end, actions) - return match - end, - }, - -- TODO(boltless): complete curl command - curl = { - impl = function (args, _) - if args[1] == "yank" then - local req_node - if not args[2] then - req_node = parser().get_cursor_request_node() - if not req_node then - logger().error("failed to find request at cursor position") - vim.notify("failed to find request at cursor position", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - else - req_node = parser().get_request_node_by_name(args[2]) - if not req_node then - logger().error("failed to find request with name:" .. args[2]) - vim.notify("failed to find request with name:" .. args[2], vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - end - local req = parser().parse(req_node, 0) - if not req then - logger().error("failed to parse request") - vim.notify("failed to parse request. See `:Rest logs` for more info", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local curl_command = require("rest-nvim.client.curl.cli").builder.build_command(req) - vim.fn.setreg("+", curl_command) - vim.notify("Copied curl command to clipboard", vim.log.levels.INFO, { title = "rest.nvim" }) - elseif args[1] == "comment" then - local req_node - if not args[2] then - req_node = parser().get_cursor_request_node() - if not req_node then - logger().error("failed to find request at cursor position") - vim.notify("failed to find request at cursor position", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - else - req_node = parser().get_request_node_by_name(args[2]) - if not req_node then - logger().error("failed to find request with name:" .. args[2]) - vim.notify("failed to find request with name:" .. args[2], vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - end - local req = parser().parse(req_node, 0) - if not req then - logger().error("failed to parse request") - vim.notify("failed to parse request. See `:Rest logs` for more info", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local curl_command = require("rest-nvim.client.curl.cli").builder.build_command(req) - local start = req_node:range() - local end_ = start - vim.api.nvim_buf_set_lines(0, start, end_, false, vim.tbl_map(function (line) return "# " .. line end, vim.split(curl_command, "\n"))) - -- elseif args[1] == "to-http" then - -- -- TODO: convert comment with curl to http request and insert it below - else - vim.notify("Invalid action '" .. args[1] .. "' provided to 'curl' command", vim.log.levels.ERROR, { title = "rest.nvim" }) - end - end, - complete = function (_args) - return { - "yank", - "comment", - -- "to-http", - } - end - } + return match + end, + }, + -- TODO(boltless): complete curl command + curl = { + impl = function(args, _) + if args[1] == "yank" then + local req_node + if not args[2] then + req_node = parser().get_cursor_request_node() + if not req_node then + logger().error("failed to find request at cursor position") + vim.notify( + "failed to find request at cursor position", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + else + req_node = parser().get_request_node_by_name(args[2]) + if not req_node then + logger().error("failed to find request with name:" .. args[2]) + vim.notify( + "failed to find request with name:" .. args[2], + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + end + local req = parser().parse(req_node, 0) + if not req then + logger().error("failed to parse request") + vim.notify( + "failed to parse request. See `:Rest logs` for more info", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + local curl_command = require("rest-nvim.client.curl.cli").builder.build_command(req) + vim.fn.setreg("+", curl_command) + vim.notify("Copied curl command to clipboard", vim.log.levels.INFO, { title = "rest.nvim" }) + elseif args[1] == "comment" then + local req_node + if not args[2] then + req_node = parser().get_cursor_request_node() + if not req_node then + logger().error("failed to find request at cursor position") + vim.notify( + "failed to find request at cursor position", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + else + req_node = parser().get_request_node_by_name(args[2]) + if not req_node then + logger().error("failed to find request with name:" .. args[2]) + vim.notify( + "failed to find request with name:" .. args[2], + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + end + local req = parser().parse(req_node, 0) + if not req then + logger().error("failed to parse request") + vim.notify( + "failed to parse request. See `:Rest logs` for more info", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + local curl_command = require("rest-nvim.client.curl.cli").builder.build_command(req) + local start = req_node:range() + local end_ = start + vim.api.nvim_buf_set_lines( + 0, + start, + end_, + false, + vim.tbl_map(function(line) + return "# " .. line + end, vim.split(curl_command, "\n")) + ) + -- elseif args[1] == "to-http" then + -- -- TODO: convert comment with curl to http request and insert it below + else + vim.notify( + "Invalid action '" .. args[1] .. "' provided to 'curl' command", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + end + end, + complete = function(_args) + return { + "yank", + "comment", + -- "to-http", + } + end, + }, } local function rest(opts) - local fargs = opts.fargs - local cmd = fargs[1] - local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {} - local command = rest_command_tbl[cmd] + local fargs = opts.fargs + local cmd = fargs[1] + local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {} + local command = rest_command_tbl[cmd] - if not command then - logger().error("Unknown command: " .. cmd) - vim.notify("Unknown command: " .. cmd, vim.log.levels.WARN, { title = "rest.nvim" }) - return - end + if not command then + logger().error("Unknown command: " .. cmd) + vim.notify("Unknown command: " .. cmd, vim.log.levels.WARN, { title = "rest.nvim" }) + return + end - command.impl(args, opts) + command.impl(args, opts) end ---@package function commands.setup() - vim.api.nvim_create_user_command("Rest", rest, { - nargs = "+", - desc = "Run your HTTP requests", - complete = function(arg_lead, cmdline, _) - local rest_commands = vim.tbl_keys(rest_command_tbl) - local subcmd, subcmd_arg_lead = cmdline:match("Rest*%s(%S+)%s(.*)$") - if subcmd and subcmd_arg_lead and rest_command_tbl[subcmd] and rest_command_tbl[subcmd].complete then - return rest_command_tbl[subcmd].complete(subcmd_arg_lead) - end - if cmdline:match("Rest*%s+%w*$") then - return vim.tbl_filter(function(cmd) - if string.find(cmd, "^" .. arg_lead) then - return cmd - ---@diagnostic disable-next-line missing-return - end - end, rest_commands) - end - end, - }) + vim.api.nvim_create_user_command("Rest", rest, { + nargs = "+", + desc = "Run your HTTP requests", + complete = function(arg_lead, cmdline, _) + local rest_commands = vim.tbl_keys(rest_command_tbl) + local subcmd, subcmd_arg_lead = cmdline:match("Rest*%s(%S+)%s(.*)$") + if subcmd and subcmd_arg_lead and rest_command_tbl[subcmd] and rest_command_tbl[subcmd].complete then + return rest_command_tbl[subcmd].complete(subcmd_arg_lead) + end + if cmdline:match("Rest*%s+%w*$") then + return vim.tbl_filter(function(cmd) + if string.find(cmd, "^" .. arg_lead) then + return cmd + ---@diagnostic disable-next-line missing-return + end + end, rest_commands) + end + end, + }) end ---Register a new `:Rest` subcommand @@ -297,10 +348,10 @@ end ---@param cmd RestCmd The implementation and optional completions ---@package function commands.register_subcommand(name, cmd) - vim.validate({ name = { name, "string" } }) - vim.validate({ impl = { cmd.impl, "function" }, complete = { cmd.complete, "function", true } }) + vim.validate({ name = { name, "string" } }) + vim.validate({ impl = { cmd.impl, "function" }, complete = { cmd.complete, "function", true } }) - rest_command_tbl[name] = cmd + rest_command_tbl[name] = cmd end return commands diff --git a/lua/rest-nvim/config/check.lua b/lua/rest-nvim/config/check.lua index d356673..4554069 100644 --- a/lua/rest-nvim/config/check.lua +++ b/lua/rest-nvim/config/check.lua @@ -13,8 +13,8 @@ local check = {} ---@return boolean is_valid ---@return string|nil error_message local function validate(tbl) - local ok, err = pcall(vim.validate, tbl) - return ok or false, "Invalid config" .. (err and ": " .. err or "") + local ok, err = pcall(vim.validate, tbl) + return ok or false, "Invalid config" .. (err and ": " .. err or "") end ---Validates the configuration @@ -22,39 +22,39 @@ end ---@return boolean is_valid ---@return string|nil error_message function check.validate(cfg) - local ok, err = validate({ - custom_dynamic_variables = { cfg.custom_dynamic_variables, "table" }, - request = { cfg.request, "table" }, - ["request.skip_ssl_verification"] = { cfg.request.skip_ssl_verification, "boolean" }, - ["request.hooks"] = { cfg.request.hooks, "table" }, - ["request.hooks.encode_url"] = { cfg.request.hooks.encode_url, "boolean" }, - ["request.hooks.user_agent"] = { cfg.request.hooks.user_agent, { "function", "string" } }, - ["request.hooks.set_content_type"] = { cfg.request.hooks.set_content_type, "boolean" }, - response = { cfg.response, "table" }, - ["response.hooks"] = { cfg.response.hooks, "table" }, - clients = { cfg.clients, "table" }, - ["clients.curl"] = { cfg.clients.curl, "table" }, - ["clients.curl.statistics"] = { cfg.clients.curl.statistics, "table" }, - cookies = { cfg.cookies, "table" }, - ["cookies.enable"] = { cfg.cookies.enable, "boolean" }, - ["cookies.path"] = { cfg.cookies.path, "string" }, - env = { cfg.env, "table" }, - ["env.enable"] = { cfg.env.enable, "boolean" }, - ["env.path"] = { cfg.env.pattern, "string" }, - ui = { cfg.ui, "table" }, - ["ui.winbar"] = { cfg.ui.winbar, "boolean" }, - ["ui.keybinds"] = { cfg.ui.keybinds, "table" }, - ["ui.keybinds.prev"] = { cfg.ui.keybinds.prev, "string" }, - ["ui.keybinds.next"] = { cfg.ui.keybinds.next, "string" }, - highlight = { cfg.highlight, "table" }, - ["highlight.enable"] = { cfg.highlight.enable, "boolean" }, - ["highlight.timeout"] = { cfg.highlight.timeout, "number" }, - }) + local ok, err = validate({ + custom_dynamic_variables = { cfg.custom_dynamic_variables, "table" }, + request = { cfg.request, "table" }, + ["request.skip_ssl_verification"] = { cfg.request.skip_ssl_verification, "boolean" }, + ["request.hooks"] = { cfg.request.hooks, "table" }, + ["request.hooks.encode_url"] = { cfg.request.hooks.encode_url, "boolean" }, + ["request.hooks.user_agent"] = { cfg.request.hooks.user_agent, { "function", "string" } }, + ["request.hooks.set_content_type"] = { cfg.request.hooks.set_content_type, "boolean" }, + response = { cfg.response, "table" }, + ["response.hooks"] = { cfg.response.hooks, "table" }, + clients = { cfg.clients, "table" }, + ["clients.curl"] = { cfg.clients.curl, "table" }, + ["clients.curl.statistics"] = { cfg.clients.curl.statistics, "table" }, + cookies = { cfg.cookies, "table" }, + ["cookies.enable"] = { cfg.cookies.enable, "boolean" }, + ["cookies.path"] = { cfg.cookies.path, "string" }, + env = { cfg.env, "table" }, + ["env.enable"] = { cfg.env.enable, "boolean" }, + ["env.path"] = { cfg.env.pattern, "string" }, + ui = { cfg.ui, "table" }, + ["ui.winbar"] = { cfg.ui.winbar, "boolean" }, + ["ui.keybinds"] = { cfg.ui.keybinds, "table" }, + ["ui.keybinds.prev"] = { cfg.ui.keybinds.prev, "string" }, + ["ui.keybinds.next"] = { cfg.ui.keybinds.next, "string" }, + highlight = { cfg.highlight, "table" }, + ["highlight.enable"] = { cfg.highlight.enable, "boolean" }, + ["highlight.timeout"] = { cfg.highlight.timeout, "number" }, + }) - if not ok then - return false, err - end - return true + if not ok then + return false, err + end + return true end ---Recursively check a table for unrecognized keys, @@ -63,31 +63,31 @@ end ---@param default_tbl table ---@return string[] function check.get_unrecognized_keys(tbl, default_tbl) - local unrecognized_keys = {} - for k, _ in pairs(tbl) do - unrecognized_keys[k] = true - end - for k, _ in pairs(default_tbl) do - unrecognized_keys[k] = false - end - - local ret = {} - for k, _ in pairs(unrecognized_keys) do - if unrecognized_keys[k] then - ret[k] = k + local unrecognized_keys = {} + for k, _ in pairs(tbl) do + unrecognized_keys[k] = true + end + for k, _ in pairs(default_tbl) do + unrecognized_keys[k] = false end - if type(default_tbl[k]) == "table" and tbl[k] then - for _, subk in pairs(check.get_unrecognized_keys(tbl[k], default_tbl[k])) do - local key = k .. "." .. subk - -- Keybinds configuration table requires a special treatment as it does not have a "static" syntax - if k ~= "keybinds" or k == "keybinds" and type(subk) ~= "number" then - ret[key] = key + + local ret = {} + for k, _ in pairs(unrecognized_keys) do + if unrecognized_keys[k] then + ret[k] = k + end + if type(default_tbl[k]) == "table" and tbl[k] then + for _, subk in pairs(check.get_unrecognized_keys(tbl[k], default_tbl[k])) do + local key = k .. "." .. subk + -- Keybinds configuration table requires a special treatment as it does not have a "static" syntax + if k ~= "keybinds" or k == "keybinds" and type(subk) ~= "number" then + ret[key] = key + end + end end - end end - end - return vim.tbl_keys(ret) + return vim.tbl_keys(ret) end return check diff --git a/lua/rest-nvim/config/default.lua b/lua/rest-nvim/config/default.lua index 00e4694..7eaca9c 100644 --- a/lua/rest-nvim/config/default.lua +++ b/lua/rest-nvim/config/default.lua @@ -3,90 +3,90 @@ ---rest.nvim default configuration ---@class rest.Config local default_config = { - ---@type table Table of custom dynamic variables - custom_dynamic_variables = {}, - ---@class rest.Config.Request - request = { - ---@type boolean Skip SSL verification, useful for unknown certificates - skip_ssl_verification = false, - ---Default request hooks - ---@class rest.Config.Request.Hooks - hooks = { - ---@type boolean Encode URL before making request - encode_url = true, - ---@type string|fun():string Set `User-Agent` header when it is empty - user_agent = function () - return "rest.nvim v" .. require("rest-nvim.api").VERSION - end, - ---@type boolean Set `Content-Type` header when it is empty and body is provided - set_content_type = true, + ---@type table Table of custom dynamic variables + custom_dynamic_variables = {}, + ---@class rest.Config.Request + request = { + ---@type boolean Skip SSL verification, useful for unknown certificates + skip_ssl_verification = false, + ---Default request hooks + ---@class rest.Config.Request.Hooks + hooks = { + ---@type boolean Encode URL before making request + encode_url = true, + ---@type string|fun():string Set `User-Agent` header when it is empty + user_agent = function() + return "rest.nvim v" .. require("rest-nvim.api").VERSION + end, + ---@type boolean Set `Content-Type` header when it is empty and body is provided + set_content_type = true, + }, }, - }, - ---@class rest.Config.Response - response = { - ---@class rest.Config.Response.Hooks - hooks = { - ---@type boolean Decode the request URL segments on response UI to improve readability - decode_url = true, - ---@type boolean Format the response body using `gq` command - format = true, + ---@class rest.Config.Response + response = { + ---@class rest.Config.Response.Hooks + hooks = { + ---@type boolean Decode the request URL segments on response UI to improve readability + decode_url = true, + ---@type boolean Format the response body using `gq` command + format = true, + }, }, - }, - ---@class rest.Config.Clients - clients = { - ---@class rest.Config.Clients.Curl - curl = { - ---Statistics to be shown, takes cURL's `--write-out` flag variables - ---See `man curl` for `--write-out` flag - ---@type table - statistics = { - time_total = { winbar = "take", title = "Time taken" }, - size_download = { winbar = "size", title = "Download size" }, - }, + ---@class rest.Config.Clients + clients = { + ---@class rest.Config.Clients.Curl + curl = { + ---Statistics to be shown, takes cURL's `--write-out` flag variables + ---See `man curl` for `--write-out` flag + ---@type table + statistics = { + time_total = { winbar = "take", title = "Time taken" }, + size_download = { winbar = "size", title = "Download size" }, + }, + }, }, - }, - ---@class rest.Config.Cookies - cookies = { - ---@type boolean Whether enable cookies support or not - enable = true, - ---@type string Cookies file path - path = vim.fs.joinpath(vim.fn.stdpath("data") --[[@as string]], "rest-nvim.cookies") - }, - ---@class rest.Config.Env - env = { - ---@type boolean - enable = true, - ---@type string - pattern = ".*%.env.*" - }, - ---@class rest.Config.UI - ui = { - ---@type boolean Whether to set winbar to result panes - winbar = true, - ---@class rest.Config.UI.Keybinds - keybinds = { - ---@type string Mapping for cycle to previous result pane - prev = "H", - ---@type string Mapping for cycle to next result pane - next = "L", + ---@class rest.Config.Cookies + cookies = { + ---@type boolean Whether enable cookies support or not + enable = true, + ---@type string Cookies file path + path = vim.fs.joinpath(vim.fn.stdpath("data") --[[@as string]], "rest-nvim.cookies"), + }, + ---@class rest.Config.Env + env = { + ---@type boolean + enable = true, + ---@type string + pattern = ".*%.env.*", + }, + ---@class rest.Config.UI + ui = { + ---@type boolean Whether to set winbar to result panes + winbar = true, + ---@class rest.Config.UI.Keybinds + keybinds = { + ---@type string Mapping for cycle to previous result pane + prev = "H", + ---@type string Mapping for cycle to next result pane + next = "L", + }, + }, + ---@class rest.Config.Highlight + highlight = { + ---@type boolean Whether current request highlighting is enabled or not + enable = true, + ---@type number Duration time of the request highlighting in milliseconds + timeout = 750, + }, + ---@see vim.log.levels + ---@type integer log level + _log_level = vim.log.levels.WARN, + ---@class rest.Config.DebugInfo + _debug_info = { + -- NOTE: default option is `nil` to prevent overwriting as empty array + ---@type string[] + unrecognized_configs = nil, }, - }, - ---@class rest.Config.Highlight - highlight = { - ---@type boolean Whether current request highlighting is enabled or not - enable = true, - ---@type number Duration time of the request highlighting in milliseconds - timeout = 750, - }, - ---@see vim.log.levels - ---@type integer log level - _log_level = vim.log.levels.WARN, - ---@class rest.Config.DebugInfo - _debug_info = { - -- NOTE: default option is `nil` to prevent overwriting as empty array - ---@type string[] - unrecognized_configs = nil, - }, } return default_config diff --git a/lua/rest-nvim/config/init.lua b/lua/rest-nvim/config/init.lua index 83a00c7..9acc60d 100644 --- a/lua/rest-nvim/config/init.lua +++ b/lua/rest-nvim/config/init.lua @@ -109,22 +109,22 @@ local default_config = require("rest-nvim.config.default") local check = require("rest-nvim.config.check") local opts = vim.g.rest_nvim or {} config = vim.tbl_deep_extend("force", { - _debug_info = { - unrecognized_configs = check.get_unrecognized_keys(opts, default_config), - }, + _debug_info = { + unrecognized_configs = check.get_unrecognized_keys(opts, default_config), + }, }, default_config, opts) ---@cast config rest.Config local ok, err = check.validate(config) if not ok then - vim.notify(err, vim.log.levels.ERROR, { title = "rest.nvim" }) + vim.notify(err, vim.log.levels.ERROR, { title = "rest.nvim" }) end if #config._debug_info.unrecognized_configs > 0 then - vim.notify( - "[rest.nvim] Unrecognized configs found in setup: " .. vim.inspect(config._debug_info.unrecognized_configs), - vim.log.levels.WARN - ) + vim.notify( + "[rest.nvim] Unrecognized configs found in setup: " .. vim.inspect(config._debug_info.unrecognized_configs), + vim.log.levels.WARN + ) end return config diff --git a/lua/rest-nvim/context.lua b/lua/rest-nvim/context.lua index 6f18920..b0969f7 100644 --- a/lua/rest-nvim/context.lua +++ b/lua/rest-nvim/context.lua @@ -20,73 +20,73 @@ math.randomseed(os.time()) ---Generate a random uuid ---@return string local function uuid() - local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" - local s = string.gsub(template, "[xy]", function(c) - local v = (c == "x") and random(0, 0xf) or random(8, 0xb) - return string.format("%x", v) - end) - return s + local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" + local s = string.gsub(template, "[xy]", function(c) + local v = (c == "x") and random(0, 0xf) or random(8, 0xb) + return string.format("%x", v) + end) + return s end ---@type table local rest_variables = { - ["$uuid"] = uuid, - ["$date"] = function() - return os.date("%Y-%m-%d") --[[@as string]] - end, - ["$timestamp"] = function() - return tostring(os.time()) or "" - end, - ["$randomInt"] = function() - return tostring(math.random(0, 1000)) - end, + ["$uuid"] = uuid, + ["$date"] = function() + return os.date("%Y-%m-%d") --[[@as string]] + end, + ["$timestamp"] = function() + return tostring(os.time()) or "" + end, + ["$randomInt"] = function() + return tostring(math.random(0, 1000)) + end, } ---@return rest.Context function Context:new() - ---@type rest.Context - local obj = { - __index = self, - linenr = 0, - vars = {}, - lv = {}, - } - setmetatable(obj, self) - return obj + ---@type rest.Context + local obj = { + __index = self, + linenr = 0, + vars = {}, + lv = {}, + } + setmetatable(obj, self) + return obj end ---@param filepath string function Context:load_file(filepath) - dotenv.load_file(filepath, function (key, value) - self:set_global(key, value) - end) + dotenv.load_file(filepath, function(key, value) + self:set_global(key, value) + end) end ---@param key string ---@param value string function Context:set_global(key, value) - vim.validate("key", key, "string") - vim.validate("value", value, "string") - self.vars[key] = value + vim.validate("key", key, "string") + vim.validate("value", value, "string") + self.vars[key] = value end ---@param key string ---@param value string function Context:set_local(key, value) - vim.validate("key", key, "string") - vim.validate("value", value, "string") - self.lv[key] = value + vim.validate("key", key, "string") + vim.validate("value", value, "string") + self.lv[key] = value end function Context:clear_local() - self.lv = {} + self.lv = {} end ---@param key string ---@return nil|fun():string local function get_dynamic_vars(key) - local user_variables = config.custom_dynamic_variables - return user_variables[key] or rest_variables[key] + local user_variables = config.custom_dynamic_variables + return user_variables[key] or rest_variables[key] end ---resolves variable @@ -97,10 +97,10 @@ end ---@param key string ---@return string value function Context:resolve(key) - -- find from dynamic variables - local var = get_dynamic_vars(key) - -- find from local variable table or vim.env - return var and var() or self.lv[key] or self.vars[key] or vim.env[key] or "" + -- find from dynamic variables + local var = get_dynamic_vars(key) + -- find from local variable table or vim.env + return var and var() or self.lv[key] or self.vars[key] or vim.env[key] or "" end M.Context = Context diff --git a/lua/rest-nvim/cookie_jar.lua b/lua/rest-nvim/cookie_jar.lua index d3c45cc..a898b37 100644 --- a/lua/rest-nvim/cookie_jar.lua +++ b/lua/rest-nvim/cookie_jar.lua @@ -1,8 +1,8 @@ ---@mod rest-nvim.cookie_jar Cookie handler module local M = { - ---@type rest.Cookie[] - jar = {}, + ---@type rest.Cookie[] + jar = {}, } local utils = require("rest-nvim.utils") @@ -23,36 +23,36 @@ local config = require("rest-nvim.config") ---Load Cookie jar from rest-nvim.cookies file function M.load_jar() - if not utils.file_exists(config.cookies.path) then - return - end - local file, openerr = io.open(config.cookies.path, "r") - if not file then - local err_msg = string.format("Failed to open rest.nvim cookies file: %s", openerr) - vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) - logger.error(err_msg) - return - end - for line in file:lines() do - local seps = vim.split(line, "\t") - if seps[1] ~= "" and not vim.startswith(seps[1], "#") then - if #seps ~= 5 then - local err_msg = "error while parsing cookies file at line:\n" .. line .. "\n" + if not utils.file_exists(config.cookies.path) then + return + end + local file, openerr = io.open(config.cookies.path, "r") + if not file then + local err_msg = string.format("Failed to open rest.nvim cookies file: %s", openerr) vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) logger.error(err_msg) return - end - ---@type rest.Cookie - local cookie = { - domain = seps[1], - path = seps[2], - name = seps[3], - value = seps[4], - expires = assert(tonumber(seps[5])), - } - table.insert(M.jar, cookie) end - end + for line in file:lines() do + local seps = vim.split(line, "\t") + if seps[1] ~= "" and not vim.startswith(seps[1], "#") then + if #seps ~= 5 then + local err_msg = "error while parsing cookies file at line:\n" .. line .. "\n" + vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) + logger.error(err_msg) + return + end + ---@type rest.Cookie + local cookie = { + domain = seps[1], + path = seps[2], + name = seps[3], + value = seps[4], + expires = assert(tonumber(seps[5])), + } + table.insert(M.jar, cookie) + end + end end ---parse url to domain and path @@ -61,12 +61,12 @@ end ---@return string domain ---@return string path local function parse_url(url) - local domain, path = url:match("^https?://([^/]+)(/[^?#]*)$") - if not path then - domain = url:match("^https?://([^/]+)") - path = "/" - end - return domain, path + local domain, path = url:match("^https?://([^/]+)(/[^?#]*)$") + if not path then + domain = url:match("^https?://([^/]+)") + path = "/" + end + return domain, path end ---@private @@ -75,60 +75,60 @@ end ---@param header string ---@return rest.Cookie? function M.parse_set_cookie(req_url, header) - local name, value = header:match("^%s*([^=]+)=([^;]*)") - if not name then - logger.error("Invalid Set-Cookie header: " .. header) - return - end - local cookie = { - name = name, - value = value or "", - } - for attr, val in header:gmatch(";%s*([^=]+)=?([^;]*)") do - attr = attr:lower() - if attr == "domain" then - cookie.domain = val - elseif attr == "path" then - cookie.path = val - elseif attr == "expires" then - cookie.expires = utils.parse_http_time(val) - elseif attr == "max-age" then - cookie.max_age = tonumber(val) - elseif attr == "secure" then - cookie.secure = true - elseif attr == "httponly" then - cookie.httponly = true - elseif attr == "samesite" then - cookie.samesite = val - elseif attr == "priority" then - cookie.priority = val + local name, value = header:match("^%s*([^=]+)=([^;]*)") + if not name then + logger.error("Invalid Set-Cookie header: " .. header) + return + end + local cookie = { + name = name, + value = value or "", + } + for attr, val in header:gmatch(";%s*([^=]+)=?([^;]*)") do + attr = attr:lower() + if attr == "domain" then + cookie.domain = val + elseif attr == "path" then + cookie.path = val + elseif attr == "expires" then + cookie.expires = utils.parse_http_time(val) + elseif attr == "max-age" then + cookie.max_age = tonumber(val) + elseif attr == "secure" then + cookie.secure = true + elseif attr == "httponly" then + cookie.httponly = true + elseif attr == "samesite" then + cookie.samesite = val + elseif attr == "priority" then + cookie.priority = val + end end - end - cookie.domain = cookie.domain or req_url:match("^https?://([^/]+)") - cookie.domain = "." .. cookie.domain - cookie.path = cookie.path or "/" - cookie.expires = cookie.expires or -1 - return cookie + cookie.domain = cookie.domain or req_url:match("^https?://([^/]+)") + cookie.domain = "." .. cookie.domain + cookie.path = cookie.path or "/" + cookie.expires = cookie.expires or -1 + return cookie end ---@param jar rest.Cookie[] ---@param cookie rest.Cookie local function jar_insert(jar, cookie) - for i, c in ipairs(jar) do - if c.name == cookie.name and c.domain == cookie.domain and c.path == cookie.path then - jar[i] = cookie - return + for i, c in ipairs(jar) do + if c.name == cookie.name and c.domain == cookie.domain and c.path == cookie.path then + jar[i] = cookie + return + end end - end - table.insert(jar, cookie) + table.insert(jar, cookie) end ---@param fn function ---@param arg any local function curry(fn, arg) - return function(...) - return fn(arg, ...) - end + return function(...) + return fn(arg, ...) + end end ---Save cookies from response @@ -136,77 +136,81 @@ end ---@param req_url string ---@param res rest.Response function M.update_jar(req_url, res) - if not res.headers["set-cookie"] then - return - end - vim.iter(res.headers["set-cookie"]):map(curry(M.parse_set_cookie, req_url)):each(curry(jar_insert, M.jar)) - M.clean() - M.save_jar() + if not res.headers["set-cookie"] then + return + end + vim.iter(res.headers["set-cookie"]):map(curry(M.parse_set_cookie, req_url)):each(curry(jar_insert, M.jar)) + M.clean() + M.save_jar() end ---@private ---Cleanup expired cookies function M.clean() - M.jar = vim - .iter(M.jar) - :filter(function(cookie) - return cookie.max_age == 0 or cookie.expires < os.time() - end) - :totable() + M.jar = vim.iter(M.jar) + :filter(function(cookie) + return cookie.max_age == 0 or cookie.expires < os.time() + end) + :totable() end ---Save current cookie jar to cookies file function M.save_jar() - -- TOOD: make this function asynchronous - local file, openerr = io.open(config.cookies.path, "w") - if not file then - local err_msg = string.format("Failed to open rest.nvim cookies file: %s", openerr) - vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) - logger.error(err_msg) - return - end - file:write("# domain\tpath\tname\tvalue\texpires\n") - for _, cookie in ipairs(M.jar) do - file:write(table.concat({ - cookie.domain, - cookie.path, - cookie.name, - cookie.value, - cookie.expires, - }, "\t") .. "\n") - end - file:close() + -- TOOD: make this function asynchronous + local file, openerr = io.open(config.cookies.path, "w") + if not file then + local err_msg = string.format("Failed to open rest.nvim cookies file: %s", openerr) + vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) + logger.error(err_msg) + return + end + file:write("# domain\tpath\tname\tvalue\texpires\n") + for _, cookie in ipairs(M.jar) do + file:write(table.concat({ + cookie.domain, + cookie.path, + cookie.name, + cookie.value, + cookie.expires, + }, "\t") .. "\n") + end + file:close() end local function match_cookie(url, cookie) - local req_domain, req_path = parse_url(url) - if not req_domain then - return false - end - local domain_matches = ("." .. req_domain):match(vim.pesc(cookie.domain) .. "$") - local path_matches = req_path:sub(1, #cookie.path) == cookie.path - if domain_matches and path_matches then - logger.debug( - ("cookie %s with domain %s and path %s matched to url: %s"):format(cookie.name, cookie.domain, cookie.path, url) - ) - else - logger.debug( - ("cookie %s with domain %s and path %s NOT matched to url: %s"):format( - cookie.name, - cookie.domain, - cookie.path, - url - ) - ) - end - return domain_matches and path_matches + local req_domain, req_path = parse_url(url) + if not req_domain then + return false + end + local domain_matches = ("." .. req_domain):match(vim.pesc(cookie.domain) .. "$") + local path_matches = req_path:sub(1, #cookie.path) == cookie.path + if domain_matches and path_matches then + logger.debug( + ("cookie %s with domain %s and path %s matched to url: %s"):format( + cookie.name, + cookie.domain, + cookie.path, + url + ) + ) + else + logger.debug( + ("cookie %s with domain %s and path %s NOT matched to url: %s"):format( + cookie.name, + cookie.domain, + cookie.path, + url + ) + ) + end + return domain_matches and path_matches end ---Load cookies for request ---@param req rest.Request function M.load_cookies(req) - logger.debug("loading cookies for request:" .. req.url) - vim.iter(M.jar):filter(curry(match_cookie, req.url)):each(curry(jar_insert, req.cookies)) + logger.debug("loading cookies for request:" .. req.url) + vim.iter(M.jar):filter(curry(match_cookie, req.url)):each(curry(jar_insert, req.cookies)) end M.load_jar() diff --git a/lua/rest-nvim/dotenv.lua b/lua/rest-nvim/dotenv.lua index 8f3bed5..28c9639 100644 --- a/lua/rest-nvim/dotenv.lua +++ b/lua/rest-nvim/dotenv.lua @@ -11,19 +11,19 @@ local config = require("rest-nvim.config") ---@param path string file path of dotenv file ---@param setter? fun(key:string, value:string) function M.load_file(path, setter) - vim.validate({ - path = { path, "string" }, - settter = { setter, { "function", "nil" }}, - }) - if not setter then - setter = function (key, value) - vim.env[key] = value + vim.validate({ + path = { path, "string" }, + settter = { setter, { "function", "nil" } }, + }) + if not setter then + setter = function(key, value) + vim.env[key] = value + end + end + local ok = dotenv_parser.parse(path, setter) + if not ok then + vim.notify("failed to load file '" .. path .. "'", vim.log.levels.WARN, { title = "rest.nvim" }) end - end - local ok = dotenv_parser.parse(path, setter) - if not ok then - vim.notify("failed to load file '" .. path .. "'", vim.log.levels.WARN, { title = "rest.nvim" }) - end end ---register the dotenv file. @@ -32,85 +32,89 @@ end ---@param path string file path of dotenv file ---@param bufnr number? buffer identifier, default to current buffer function M.register_file(path, bufnr) - vim.validate({ - path = { - path, - function (p) - return vim.endswith(p, ".env") or vim.endswith(p, ".json") - end, - "`.env` or `.json` filetype" - } - }) - bufnr = bufnr or 0 - vim.b[bufnr]._rest_nvim_env_file = path - vim.notify("Env file '" .. path .. "' has been registered", vim.log.levels.INFO, { title = "rest.nvim" }) + vim.validate({ + path = { + path, + function(p) + return vim.endswith(p, ".env") or vim.endswith(p, ".json") + end, + "`.env` or `.json` filetype", + }, + }) + bufnr = bufnr or 0 + vim.b[bufnr]._rest_nvim_env_file = path + vim.notify("Env file '" .. path .. "' has been registered", vim.log.levels.INFO, { title = "rest.nvim" }) end ---show registered dotenv file for current buffer ---@param bufnr number? buffer identifier, default to current buffer function M.show_registered_file(bufnr) - bufnr = bufnr or 0 - if not vim.b[bufnr]._rest_nvim_env_file then - vim.notify("No env file is used in current buffer", vim.log.levels.WARN, { title = "rest.nvim" }) - else - vim.notify("Current env file in use: " .. vim.b._rest_nvim_env_file, vim.log.levels.INFO, { title = "rest.nvim" }) - end + bufnr = bufnr or 0 + if not vim.b[bufnr]._rest_nvim_env_file then + vim.notify("No env file is used in current buffer", vim.log.levels.WARN, { title = "rest.nvim" }) + else + vim.notify( + "Current env file in use: " .. vim.b._rest_nvim_env_file, + vim.log.levels.INFO, + { title = "rest.nvim" } + ) + end end ---Find a list of environment files starting from the current directory ---@return string[] files Environment variable files path function M.find_env_files() - -- We are currently looking for any ".*env*" file, e.g. ".env", ".env.json" - -- - -- This algorithm can be improved later on to search from a parent directory if the desired environment file - -- is somewhere else but in the current working directory. - local files = vim.fs.find(function(name, _) - return name:match(config.env.pattern) - end, { limit = math.huge, type = "file", path = "./" }) + -- We are currently looking for any ".*env*" file, e.g. ".env", ".env.json" + -- + -- This algorithm can be improved later on to search from a parent directory if the desired environment file + -- is somewhere else but in the current working directory. + local files = vim.fs.find(function(name, _) + return name:match(config.env.pattern) + end, { limit = math.huge, type = "file", path = "./" }) - return files + return files end ---@return string? dotenv file function M.find_relevent_env_file() - local filename = vim.fn.expand("%:t:r") - if filename == "" then - return nil - end - filename = filename:gsub("%.http$", "") - ---@type string? - local env_file - -- search for `/same/path/filename.env` - env_file = vim.fs.find(filename .. ".env", { type = "file" })[1] - if env_file then - return env_file - end - -- search upward for `.env` file - env_file = vim.fs.find(function (name, _) - return name == ".env" - end, { - path = vim.fn.expand("%:h"), - upward = true, - stop = vim.fn.getcwd(), - type = "file", - limit = math.huge, - })[1] - if env_file then - return env_file - end + local filename = vim.fn.expand("%:t:r") + if filename == "" then + return nil + end + filename = filename:gsub("%.http$", "") + ---@type string? + local env_file + -- search for `/same/path/filename.env` + env_file = vim.fs.find(filename .. ".env", { type = "file" })[1] + if env_file then + return env_file + end + -- search upward for `.env` file + env_file = vim.fs.find(function(name, _) + return name == ".env" + end, { + path = vim.fn.expand("%:h"), + upward = true, + stop = vim.fn.getcwd(), + type = "file", + limit = math.huge, + })[1] + if env_file then + return env_file + end end ---@param bufnr number? buffer identifier, default to current buffer ---@return string? path function M.select_file(bufnr) - bufnr = bufnr or vim.api.nvim_get_current_buf() - vim.ui.select(M.find_env_files(), { - prompt = 'Select env files', - }, function (item, _idx) - if item then - M.register_file(item, bufnr) - end - end) + bufnr = bufnr or vim.api.nvim_get_current_buf() + vim.ui.select(M.find_env_files(), { + prompt = "Select env files", + }, function(item, _idx) + if item then + M.register_file(item, bufnr) + end + end) end return M diff --git a/lua/rest-nvim/health.lua b/lua/rest-nvim/health.lua index 00af0f9..c1fc51b 100644 --- a/lua/rest-nvim/health.lua +++ b/lua/rest-nvim/health.lua @@ -11,64 +11,67 @@ local health = {} local config = require("rest-nvim.config") local function install_health() - vim.health.start("Installation") + vim.health.start("Installation") - -- Luarocks installed - -- we check for either luarocks system-wide or rocks.nvim as rocks.nvim can manage Luarocks installation - -- and also luarocks.nvim in case the end-user is using lazy.nvim - local found_luarocks_nvim = package.searchpath("luarocks", package.path) + -- Luarocks installed + -- we check for either luarocks system-wide or rocks.nvim as rocks.nvim can manage Luarocks installation + -- and also luarocks.nvim in case the end-user is using lazy.nvim + local found_luarocks_nvim = package.searchpath("luarocks", package.path) - if vim.fn.executable("luarocks") ~= 1 and not vim.g.rocks_nvim_loaded and not found_luarocks_nvim then - vim.health.warn("`Luarocks` is not installed in your system", "Are you sure you installed all needed dependencies properly?") - else - vim.health.ok("Found `luarocks` installed in your system") - end + if vim.fn.executable("luarocks") ~= 1 and not vim.g.rocks_nvim_loaded and not found_luarocks_nvim then + vim.health.warn( + "`Luarocks` is not installed in your system", + "Are you sure you installed all needed dependencies properly?" + ) + else + vim.health.ok("Found `luarocks` installed in your system") + end - -- Luarocks in `package.path` - local found_luarocks_in_path = string.find(package.path, "rocks") - if not found_luarocks_in_path then - vim.health.error( - "Luarocks PATHs were not found in your Neovim's Lua `package.path`", - "Check rest.nvim README to know how to add your luarocks PATHs to Neovim" - ) - else - vim.health.ok("Found Luarocks PATHs in your Neovim's Lua `package.path`") - end + -- Luarocks in `package.path` + local found_luarocks_in_path = string.find(package.path, "rocks") + if not found_luarocks_in_path then + vim.health.error( + "Luarocks PATHs were not found in your Neovim's Lua `package.path`", + "Check rest.nvim README to know how to add your luarocks PATHs to Neovim" + ) + else + vim.health.ok("Found Luarocks PATHs in your Neovim's Lua `package.path`") + end - -- Luarocks dependencies existence checking - for dep, dep_info in pairs(vim.g.rest_nvim_deps) do - if not dep_info.found then - local err_advice = "Install it through `luarocks --local --lua-version=5.1 install " .. dep .. "`" - if dep:find("nvim") then - err_advice = "Install it through your preferred plugins manager or luarocks by using `luarocks --local --lua-version=5.1 install " - .. dep - .. "`" - end + -- Luarocks dependencies existence checking + for dep, dep_info in pairs(vim.g.rest_nvim_deps) do + if not dep_info.found then + local err_advice = "Install it through `luarocks --local --lua-version=5.1 install " .. dep .. "`" + if dep:find("nvim") then + err_advice = "Install it through your preferred plugins manager or luarocks by using `luarocks --local --lua-version=5.1 install " + .. dep + .. "`" + end - vim.health.error("Dependency `" .. dep .. "` was not found (" .. dep_info.error .. ")", err_advice) - else - vim.health.ok("Dependency `" .. dep .. "` was found") + vim.health.error("Dependency `" .. dep .. "` was not found (" .. dep_info.error .. ")", err_advice) + else + vim.health.ok("Dependency `" .. dep .. "` was found") + end end - end end local function configuration_health() - vim.health.start("Configuration") + vim.health.start("Configuration") - -- Configuration options - local unrecognized_configs = config._debug_info.unrecognized_configs - if not vim.tbl_isempty(unrecognized_configs) then - for _, config_key in ipairs(unrecognized_configs) do - vim.health.warn("Unrecognized configuration option `" .. config_key .. "` found") + -- Configuration options + local unrecognized_configs = config._debug_info.unrecognized_configs + if not vim.tbl_isempty(unrecognized_configs) then + for _, config_key in ipairs(unrecognized_configs) do + vim.health.warn("Unrecognized configuration option `" .. config_key .. "` found") + end + else + vim.health.ok("No unrecognized configuration options were found") end - else - vim.health.ok("No unrecognized configuration options were found") - end end function health.check() - install_health() - configuration_health() + install_health() + configuration_health() end return health diff --git a/lua/rest-nvim/init.lua b/lua/rest-nvim/init.lua index 76ed061..b78ff10 100644 --- a/lua/rest-nvim/init.lua +++ b/lua/rest-nvim/init.lua @@ -15,8 +15,8 @@ local rest = {} ---This api does nothing but set `vim.g.rest_nvim` to `user_configs` ---@param user_configs? rest.Opts User configurations function rest.setup(user_configs) - -- Set up rest.nvim configurations - vim.g.rest_nvim = user_configs or {} + -- Set up rest.nvim configurations + vim.g.rest_nvim = user_configs or {} end return rest diff --git a/lua/rest-nvim/logger.lua b/lua/rest-nvim/logger.lua index 26884e1..0e5a070 100644 --- a/lua/rest-nvim/logger.lua +++ b/lua/rest-nvim/logger.lua @@ -30,7 +30,7 @@ local log_date_format = "%F %H:%M:%S" ---@package ---@return string filepath function logger.get_logfile() - return vim.fs.joinpath(default_log_path, "rest-nvim.log") + return vim.fs.joinpath(default_log_path, "rest-nvim.log") end local logfile, openerr @@ -38,79 +38,79 @@ local logfile, openerr ---Opens log file. Returns true if file is open, false on error ---@return boolean local function open_logfile() - -- Try to open file only once - if logfile then - return true - end - if openerr then - return false - end + -- Try to open file only once + if logfile then + return true + end + if openerr then + return false + end - vim.fn.mkdir(default_log_path, "-p") - logfile, openerr = io.open(logger.get_logfile(), "w+") - if not logfile then - local err_msg = string.format("Failed to open rest.nvim log file: %s", openerr) - vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) - return false - end + vim.fn.mkdir(default_log_path, "-p") + logfile, openerr = io.open(logger.get_logfile(), "w+") + if not logfile then + local err_msg = string.format("Failed to open rest.nvim log file: %s", openerr) + vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) + return false + end - local log_info = vim.uv.fs_stat(logger.get_logfile()) - if log_info and log_info.size > LARGE then - local warn_msg = - string.format("rest.nvim log is large (%d MB): %s", log_info.size / (1000 * 1000), logger.get_logfile()) - vim.notify(warn_msg, vim.log.levels.WARN, { title = "rest.nvim" }) - end + local log_info = vim.uv.fs_stat(logger.get_logfile()) + if log_info and log_info.size > LARGE then + local warn_msg = + string.format("rest.nvim log is large (%d MB): %s", log_info.size / (1000 * 1000), logger.get_logfile()) + vim.notify(warn_msg, vim.log.levels.WARN, { title = "rest.nvim" }) + end - -- Start message for logging - logfile:write(string.format("[START][%s] rest.nvim logging initiated\n", os.date(log_date_format))) - return true + -- Start message for logging + logfile:write(string.format("[START][%s] rest.nvim logging initiated\n", os.date(log_date_format))) + return true end local log_levels = vim.deepcopy(vim.log.levels) for levelstr, levelnr in pairs(log_levels) do - log_levels[levelnr] = levelstr + log_levels[levelnr] = levelstr end ---Set the log level for the logger ---@param level (string|integer) New logging level ---@see vim.log.levels function logger.set_log_level(level) - if type(level) == "string" then - logger.level = assert(log_levels[level:upper()], string.format("rest.nvim: Invalid log level: %q", level)) - else - assert(log_levels[level], string.format("rest.nvim: Invalid log level: %d", level)) - logger.level = level - end + if type(level) == "string" then + logger.level = assert(log_levels[level:upper()], string.format("rest.nvim: Invalid log level: %q", level)) + else + assert(log_levels[level], string.format("rest.nvim: Invalid log level: %d", level)) + logger.level = level + end end for level, levelnr in pairs(vim.log.levels) do - logger[level:lower()] = function(...) - if logger.level == vim.log.levels.OFF or not open_logfile() then - return false - end - local argc = select("#", ...) - if levelnr < logger.level then - return false - end - if argc == 0 then - return true - end - local info = debug.getinfo(2, "Sl") - local fileinfo = string.format("%s:%s", info.short_src, info.currentline) - local parts = { level, "|", os.date(log_date_format), "|", fileinfo, "|" } - for i = 1, argc do - local arg = select(i, ...) - if arg == nil then - table.insert(parts, "") - elseif type(arg) == "string" then - table.insert(parts, arg) - else - table.insert(parts, vim.inspect(arg)) - end + logger[level:lower()] = function(...) + if logger.level == vim.log.levels.OFF or not open_logfile() then + return false + end + local argc = select("#", ...) + if levelnr < logger.level then + return false + end + if argc == 0 then + return true + end + local info = debug.getinfo(2, "Sl") + local fileinfo = string.format("%s:%s", info.short_src, info.currentline) + local parts = { level, "|", os.date(log_date_format), "|", fileinfo, "|" } + for i = 1, argc do + local arg = select(i, ...) + if arg == nil then + table.insert(parts, "") + elseif type(arg) == "string" then + table.insert(parts, arg) + else + table.insert(parts, vim.inspect(arg)) + end + end + logfile:write(table.concat(parts, " "), "\n") + logfile:flush() end - logfile:write(table.concat(parts, " "), "\n") - logfile:flush() - end end logger.set_log_level(vim.tbl_get(vim.g, "rest_nvim", "_log_level") or vim.log.levels.WARN) diff --git a/lua/rest-nvim/parser/dotenv.lua b/lua/rest-nvim/parser/dotenv.lua index eec622d..eeb2093 100644 --- a/lua/rest-nvim/parser/dotenv.lua +++ b/lua/rest-nvim/parser/dotenv.lua @@ -7,26 +7,26 @@ local M = {} ---@param path string The environment file path ---@return string|nil local function get_filetype(path) - local ext = vim.fn.fnamemodify(path, ":e") - return ext == "" and nil or ext + local ext = vim.fn.fnamemodify(path, ":e") + return ext == "" and nil or ext end ---@param value any ---@return string local function value_tostring(value) - vim.validate({ - value = { - value, - function (v) - return v == vim.NIL or vim.tbl_contains({ "nil", "number", "string", "boolean" }, type(value)) - end, - "vim.NIL|nil|bumber|string|boolean" - } - }) - if value == vim.NIL or value == nil then - return "" - end - return tostring(value) + vim.validate({ + value = { + value, + function(v) + return v == vim.NIL or vim.tbl_contains({ "nil", "number", "string", "boolean" }, type(value)) + end, + "vim.NIL|nil|bumber|string|boolean", + }, + }) + if value == vim.NIL or value == nil then + return "" + end + return tostring(value) end ---parse dotenv file @@ -37,48 +37,48 @@ end ---@return boolean ok ---@return table|nil function M.parse(path, setter) - local vars - if not setter then - vars = {} - setter = function (key, value) - vars[key] = value - end - end - if not utils.file_exists(path) then - logger.error("Current environment file '" .. path .. "' was not found") - return false - end - local env_ext = get_filetype(path) - local file_contents = utils.read_file(path) - if env_ext == "json" then - local ok, json_tbl = pcall(vim.json.decode, file_contents) - if not ok or type(json_tbl) ~= "table" or vim.islist(json_tbl) then - logger.error("failed parsing json data") - return false + local vars + if not setter then + vars = {} + setter = function(key, value) + vars[key] = value + end end - for key, value in pairs(json_tbl) do - if type(key) == "string" and type(value) ~= "table" then - setter(key, value_tostring(value)) - end + if not utils.file_exists(path) then + logger.error("Current environment file '" .. path .. "' was not found") + return false end - else - local vars_tbl = vim.split(file_contents, "\n") - table.remove(vars_tbl, #vars_tbl) - for _, var in ipairs(vars_tbl) do - local variable = vim.split(var, "=") - local variable_name = variable[1] - local variable_value - -- In case some weirdo adds a `=` character to his ENV value - if #variable > 2 then - table.remove(variable, 1) - variable_value = table.concat(variable, "=") - else - variable_value = variable[2] - end - setter(variable_name, value_tostring(variable_value)) + local env_ext = get_filetype(path) + local file_contents = utils.read_file(path) + if env_ext == "json" then + local ok, json_tbl = pcall(vim.json.decode, file_contents) + if not ok or type(json_tbl) ~= "table" or vim.islist(json_tbl) then + logger.error("failed parsing json data") + return false + end + for key, value in pairs(json_tbl) do + if type(key) == "string" and type(value) ~= "table" then + setter(key, value_tostring(value)) + end + end + else + local vars_tbl = vim.split(file_contents, "\n") + table.remove(vars_tbl, #vars_tbl) + for _, var in ipairs(vars_tbl) do + local variable = vim.split(var, "=") + local variable_name = variable[1] + local variable_value + -- In case some weirdo adds a `=` character to his ENV value + if #variable > 2 then + table.remove(variable, 1) + variable_value = table.concat(variable, "=") + else + variable_value = variable[2] + end + setter(variable_name, value_tostring(variable_value)) + end end - end - return true, vars + return true, vars end return M diff --git a/lua/rest-nvim/parser/init.lua b/lua/rest-nvim/parser/init.lua index 6f5c03d..f3dc303 100644 --- a/lua/rest-nvim/parser/init.lua +++ b/lua/rest-nvim/parser/init.lua @@ -10,13 +10,15 @@ local parser = {} local Context = require("rest-nvim.context").Context -local utils = require("rest-nvim.utils") -local logger = require("rest-nvim.logger") +local utils = require("rest-nvim.utils") +local logger = require("rest-nvim.logger") local jar = require("rest-nvim.cookie_jar") ---@alias Source integer|string Buffer or string which the `node` is extracted -local NAMED_REQUEST_QUERY = vim.treesitter.query.parse("http", [[ +local NAMED_REQUEST_QUERY = vim.treesitter.query.parse( + "http", + [[ (section (request_separator value: (_) @name) @@ -27,15 +29,16 @@ local NAMED_REQUEST_QUERY = vim.treesitter.query.parse("http", [[ value: (_) @name (#eq? @_keyword "name")) request: (_)) @request -]]) +]] +) ---@param node TSNode ---@param field string ---@param source Source ---@return string|nil local function get_node_field_text(node, field, source) - local n = node:field(field)[1] - return n and vim.treesitter.get_node_text(n, source) or nil + local n = node:field(field)[1] + return n and vim.treesitter.get_node_text(n, source) or nil end ---@param src string @@ -44,14 +47,14 @@ end ---@return string ---@return integer local function expand_variables(src, context, encoder) - return src:gsub("{{(.-)}}", function (name) - name = vim.trim(name) - local res = context:resolve(name) - if encoder then - res = encoder(res) - end - return res - end) + return src:gsub("{{(.-)}}", function(name) + name = vim.trim(name) + local res = context:resolve(name) + if encoder then + res = encoder(res) + end + return res + end) end ---@param req_node TSNode Tree-sitter request node @@ -59,51 +62,57 @@ end ---@param context rest.Context ---@return table headers local function parse_headers(req_node, source, context) - local headers = vim.defaulttable(function () return {} end) - local header_nodes = req_node:field("header") - for _, node in ipairs(header_nodes) do - local key = assert(get_node_field_text(node, "name", source)) - local value = get_node_field_text(node, "value", source) - key = expand_variables(key, context):lower() - if value then - value = expand_variables(value, context) - table.insert(headers[key], value) - else - headers[key] = {} + local headers = vim.defaulttable(function() + return {} + end) + local header_nodes = req_node:field("header") + for _, node in ipairs(header_nodes) do + local key = assert(get_node_field_text(node, "name", source)) + local value = get_node_field_text(node, "value", source) + key = expand_variables(key, context):lower() + if value then + value = expand_variables(value, context) + table.insert(headers[key], value) + else + headers[key] = {} + end end - end - return setmetatable(headers, nil) + return setmetatable(headers, nil) end ---@param str string ---@return boolean local function validate_json(str) - local ok, _ = pcall(vim.json.decode, str) - return ok + local ok, _ = pcall(vim.json.decode, str) + return ok end ---@param str string ---@return boolean local function validate_xml(str) - local xml2lua = require("xml2lua") - local handler = require("xmlhandler.tree"):new() - local xml_parser = xml2lua.parser(handler) - local ok = pcall(function (t) return xml_parser:parse(t) end, str) - return ok + local xml2lua = require("xml2lua") + local handler = require("xmlhandler.tree"):new() + local xml_parser = xml2lua.parser(handler) + local ok = pcall(function(t) + return xml_parser:parse(t) + end, str) + return ok end ---@param str string ---@return string? local function parse_urlencoded_form(str) - local query_pairs = vim.split(str, "&") - return vim.iter(query_pairs):map(function (query) - local key, value = query:match("([^=]+)=?(.*)") - if not key then - logger.error(("Error while parsing query '%s' from urlencoded form '%s'"):format(query_pairs, str)) - return nil - end - return vim.trim(key) .. "=" .. vim.trim(value) - end):join("&") + local query_pairs = vim.split(str, "&") + return vim.iter(query_pairs) + :map(function(query) + local key, value = query:match("([^=]+)=?(.*)") + if not key then + logger.error(("Error while parsing query '%s' from urlencoded form '%s'"):format(query_pairs, str)) + return nil + end + return vim.trim(key) .. "=" .. vim.trim(value) + end) + :join("&") end ---@param content_type string? @@ -112,64 +121,64 @@ end ---@param context rest.Context ---@return rest.Request.Body? function parser.parse_body(content_type, body_node, source, context) - local body = {} - local node_type = body_node:type() - ---@cast body rest.Request.Body - if node_type == "external_body" then - body.__TYPE = "external" - local path = assert(get_node_field_text(body_node, "path", source)) - if type(source) ~= "number" then - logger.error("can't parse external body on non-existing http file") - return - end - ---@cast source integer - local basepath = vim.fs.dirname(vim.api.nvim_buf_get_name(source)) - ---@diagnostic disable-next-line: undefined-field - basepath = basepath:gsub("^" .. vim.pesc(vim.uv.cwd() .. "/"), "") - path = vim.fs.normalize(vim.fs.joinpath(basepath, path)) - body.data = { - name = get_node_field_text(body_node, "name", source), - path = path, - } - elseif node_type == "json_body" or content_type == "application/json" then - body.__TYPE = "json" - body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) - body.data = expand_variables(body.data, context) - local ok = validate_json(body.data) - if not ok then - logger.warn("invalid json: '" .. body.data .. "'") - return nil + local body = {} + local node_type = body_node:type() + ---@cast body rest.Request.Body + if node_type == "external_body" then + body.__TYPE = "external" + local path = assert(get_node_field_text(body_node, "path", source)) + if type(source) ~= "number" then + logger.error("can't parse external body on non-existing http file") + return + end + ---@cast source integer + local basepath = vim.fs.dirname(vim.api.nvim_buf_get_name(source)) + ---@diagnostic disable-next-line: undefined-field + basepath = basepath:gsub("^" .. vim.pesc(vim.uv.cwd() .. "/"), "") + path = vim.fs.normalize(vim.fs.joinpath(basepath, path)) + body.data = { + name = get_node_field_text(body_node, "name", source), + path = path, + } + elseif node_type == "json_body" or content_type == "application/json" then + body.__TYPE = "json" + body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) + body.data = expand_variables(body.data, context) + local ok = validate_json(body.data) + if not ok then + logger.warn("invalid json: '" .. body.data .. "'") + return nil + end + elseif node_type == "xml_body" or content_type == "application/xml" then + body.__TYPE = "xml" + body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) + body.data = expand_variables(body.data, context) + local ok = validate_xml(body.data) + if not ok then + logger.warn("invalid xml: '" .. body.data .. "'") + return nil + end + elseif node_type == "raw_body" then + local text = vim.treesitter.get_node_text(body_node, source) + if content_type and vim.startswith(content_type, "application/x-www-form-urlencoded") then + body.__TYPE = "raw" + body.data = parse_urlencoded_form(text) + if not body.data then + logger.error("Error while parsing urlencoded form") + return nil + end + else + body.__TYPE = "raw" + body.data = text + end + elseif node_type == "multipart_form_data" then + body.__TYPE = "multipart_form_data" + -- TODO: + logger.error("multipart form data is not supported yet") + elseif node_type == "graphql_body" then + logger.error("graphql body is not supported yet") end - elseif node_type == "xml_body" or content_type == "application/xml" then - body.__TYPE = "xml" - body.data = vim.trim(vim.treesitter.get_node_text(body_node, source)) - body.data = expand_variables(body.data, context) - local ok = validate_xml(body.data) - if not ok then - logger.warn("invalid xml: '" .. body.data .. "'") - return nil - end - elseif node_type == "raw_body" then - local text = vim.treesitter.get_node_text(body_node, source) - if content_type and vim.startswith(content_type, "application/x-www-form-urlencoded") then - body.__TYPE = "raw" - body.data = parse_urlencoded_form(text) - if not body.data then - logger.error("Error while parsing urlencoded form") - return nil - end - else - body.__TYPE = "raw" - body.data = text - end - elseif node_type == "multipart_form_data" then - body.__TYPE = "multipart_form_data" - -- TODO: - logger.error("multipart form data is not supported yet") - elseif node_type == "graphql_body" then - logger.error("graphql body is not supported yet") - end - return body + return body end ---In-place variables can be evaluated in loaded buffers due to treesitter limitations @@ -177,76 +186,76 @@ end ---@param ctx rest.Context ---@param endline number zero-based line number function parser.eval_context(source, ctx, endline) - vim.validate("source", source, "number") - local startline = ctx.linenr - for ln = startline, endline do - local start_node = vim.treesitter.get_node({ pos = { ln, 0 }}) - if start_node then - local node = utils.ts_find(start_node, "variable_declaration", true) - if node then - parser.parse_variable_declaration(node, source, ctx) - end + vim.validate("source", source, "number") + local startline = ctx.linenr + for ln = startline, endline do + local start_node = vim.treesitter.get_node({ pos = { ln, 0 } }) + if start_node then + local node = utils.ts_find(start_node, "variable_declaration", true) + if node then + parser.parse_variable_declaration(node, source, ctx) + end + end end - end end ---@return TSNode? node TSNode with type `section` function parser.get_cursor_request_node() - local node = vim.treesitter.get_node() - if node then - node = utils.ts_find(node, "section") - if not node then - logger.error("can't find request section node") - return - elseif node:has_error() then - logger.error(utils.ts_node_error_log(node)) - return - elseif #node:field("request") < 1 then - logger.error("request section doesn't have request node") - return + local node = vim.treesitter.get_node() + if node then + node = utils.ts_find(node, "section") + if not node then + logger.error("can't find request section node") + return + elseif node:has_error() then + logger.error(utils.ts_node_error_log(node)) + return + elseif #node:field("request") < 1 then + logger.error("request section doesn't have request node") + return + end end - end - return node + return node end ---@param source Source ---@return TSNode[] function parser.get_all_request_nodes(source) - local _, tree = utils.ts_parse_source(source) - local result = {} - for node, _ in tree:root():iter_children() do - if node:type() == "section" and #node:field("request") > 0 then - table.insert(result, node) + local _, tree = utils.ts_parse_source(source) + local result = {} + for node, _ in tree:root():iter_children() do + if node:type() == "section" and #node:field("request") > 0 then + table.insert(result, node) + end end - end - return result + return result end ---@return TSNode? function parser.get_request_node_by_name(name) - local source = 0 - local _, tree = utils.ts_parse_source(source) - local query = NAMED_REQUEST_QUERY - for id, node, _metadata, _match in query:iter_captures(tree:root(), source) do - local capture_name = query.captures[id] - if capture_name == "name" and vim.treesitter.get_node_text(node, source) == name then - local find = utils.ts_find(node, "section") - if find then - return find - end + local source = 0 + local _, tree = utils.ts_parse_source(source) + local query = NAMED_REQUEST_QUERY + for id, node, _metadata, _match in query:iter_captures(tree:root(), source) do + local capture_name = query.captures[id] + if capture_name == "name" and vim.treesitter.get_node_text(node, source) == name then + local find = utils.ts_find(node, "section") + if find then + return find + end + end end - end end ---@param vd_node TSNode ---@param source Source ---@param ctx rest.Context function parser.parse_variable_declaration(vd_node, source, ctx) - vim.validate({ node = utils.ts_node_spec(vd_node, "variable_declaration") }) - local name = assert(get_node_field_text(vd_node, "name", source)) - local value = vim.trim(assert(get_node_field_text(vd_node, "value", source))) - value = expand_variables(value, ctx) - ctx:set_global(name, value) + vim.validate({ node = utils.ts_node_spec(vd_node, "variable_declaration") }) + local name = assert(get_node_field_text(vd_node, "name", source)) + local value = vim.trim(assert(get_node_field_text(vd_node, "value", source))) + value = expand_variables(value, ctx) + ctx:set_global(name, value) end ---@param node TSNode @@ -254,31 +263,31 @@ end ---@return string lang ---@return string str local function parse_script(node, source) - local lang = "javascript" - local prev_node = utils.ts_upper_node(node) - if prev_node and prev_node:type() == "comment" and get_node_field_text(prev_node, "name", source) == "lang" then - local value = get_node_field_text(prev_node, "value", source) - if value then - lang = value + local lang = "javascript" + local prev_node = utils.ts_upper_node(node) + if prev_node and prev_node:type() == "comment" and get_node_field_text(prev_node, "name", source) == "lang" then + local value = get_node_field_text(prev_node, "value", source) + if value then + lang = value + end end - end - local script_node = assert(node:named_child(0)) - local str = vim.treesitter.get_node_text(script_node, source):sub(3,-3) - return lang, str + local script_node = assert(node:named_child(0)) + local str = vim.treesitter.get_node_text(script_node, source):sub(3, -3) + return lang, str end ---@param node TSNode ---@param source Source ---@param context rest.Context function parser.parse_pre_request_script(node, source, context) - local lang, str = parse_script(node, source) - local ok, script = pcall(require, "rest-nvim.script." .. lang) - if not ok then - logger.error(("failed to load script with language '%s'. Can't find script runner client."):format(lang)) - return - end - ---@cast script rest.ScriptClient - script.load_pre_req_hook(str, context)() + local lang, str = parse_script(node, source) + local ok, script = pcall(require, "rest-nvim.script." .. lang) + if not ok then + logger.error(("failed to load script with language '%s'. Can't find script runner client."):format(lang)) + return + end + ---@cast script rest.ScriptClient + script.load_pre_req_hook(str, context)() end ---@param node TSNode @@ -286,29 +295,29 @@ end ---@param context rest.Context ---@return function? function parser.parse_request_handler(node, source, context) - local lang, str = parse_script(node, source) - local ok, script = pcall(require, "rest-nvim.script." .. lang) - if not ok then - logger.error(("failed to load script with language '%s'. Can't find script runner client."):format(lang)) - return - end - ---@cast script rest.ScriptClient - return script.load_post_req_hook(str, context) + local lang, str = parse_script(node, source) + local ok, script = pcall(require, "rest-nvim.script." .. lang) + if not ok then + logger.error(("failed to load script with language '%s'. Can't find script runner client."):format(lang)) + return + end + ---@cast script rest.ScriptClient + return script.load_post_req_hook(str, context) end ---@param source Source ---@return string[] function parser.get_request_names(source) - local _, tree = utils.ts_parse_source(source) - local query = NAMED_REQUEST_QUERY - local result = {} - for id, node, _metadata, _match in query:iter_captures(tree:root(), source) do - local capture_name = query.captures[id] - if capture_name == "name" then - table.insert(result, vim.treesitter.get_node_text(node, source)) + local _, tree = utils.ts_parse_source(source) + local query = NAMED_REQUEST_QUERY + local result = {} + for id, node, _metadata, _match in query:iter_captures(tree:root(), source) do + local capture_name = query.captures[id] + if capture_name == "name" then + table.insert(result, vim.treesitter.get_node_text(node, source)) + end end - end - return result + return result end ---Parse the request node and create Request object. Returns `nil` if parsing @@ -318,107 +327,107 @@ end ---@param ctx? rest.Context ---@return rest.Request|nil function parser.parse(node, source, ctx) - assert(node:type() == "section") - assert(not node:has_error()) - local req_node = node:field("request")[1] - assert(req_node) + assert(node:type() == "section") + assert(not node:has_error()) + local req_node = node:field("request")[1] + assert(req_node) - ctx = ctx or Context:new() - -- TODO: note that in-place variables won't be evaluated due to treesitter limitations - -- when source is given as raw string - if type(source) == "number" then - local start_row = node:range() - parser.eval_context(source, ctx, start_row) - end - local method = get_node_field_text(req_node, "method", source) - if not method then - logger.info("no method provided, falling back to 'GET'") - method = "GET" - end - -- NOTE: url will be parsed after because in-place variables should be parsed - -- first - local url + ctx = ctx or Context:new() + -- TODO: note that in-place variables won't be evaluated due to treesitter limitations + -- when source is given as raw string + if type(source) == "number" then + local start_row = node:range() + parser.eval_context(source, ctx, start_row) + end + local method = get_node_field_text(req_node, "method", source) + if not method then + logger.info("no method provided, falling back to 'GET'") + method = "GET" + end + -- NOTE: url will be parsed after because in-place variables should be parsed + -- first + local url - local name - local handlers = {} - for child, _ in node:iter_children() do - local child_type = child:type() - if child_type == "request" then - url = expand_variables( - assert(get_node_field_text(req_node, "url", source)), - ctx, - utils.escape - ) - url = url:gsub("\n%s+", "") - elseif child_type == "pre_request_script" then - parser.parse_pre_request_script(child, source, ctx) - elseif child_type == "res_handler_script" then - local handler = parser.parse_request_handler(child, source, ctx) - if handler then - table.insert(handlers, handler) - end - elseif child_type == "request_separator" then - name = get_node_field_text(child, "value", source) - elseif child_type == "comment" and get_node_field_text(child, "name", source) == "name" then - name = get_node_field_text(child, "value", source) or name - elseif child_type == "variable_declaration" then - parser.parse_variable_declaration(child, source, ctx) + local name + local handlers = {} + for child, _ in node:iter_children() do + local child_type = child:type() + if child_type == "request" then + url = expand_variables(assert(get_node_field_text(req_node, "url", source)), ctx, utils.escape) + url = url:gsub("\n%s+", "") + elseif child_type == "pre_request_script" then + parser.parse_pre_request_script(child, source, ctx) + elseif child_type == "res_handler_script" then + local handler = parser.parse_request_handler(child, source, ctx) + if handler then + table.insert(handlers, handler) + end + elseif child_type == "request_separator" then + name = get_node_field_text(child, "value", source) + elseif child_type == "comment" and get_node_field_text(child, "name", source) == "name" then + name = get_node_field_text(child, "value", source) or name + elseif child_type == "variable_declaration" then + parser.parse_variable_declaration(child, source, ctx) + end end - end - if not name then - if type(source) == "number" then - local filename = vim.api.nvim_buf_get_name(source) - name = filename:match(".*/%.?(.*).http$") or filename - name = name .. "#" .. vim.b[source]._rest_nvim_count - vim.b[source]._rest_nvim_count = vim.b[source]._rest_nvim_count + 1 + if not name then + if type(source) == "number" then + local filename = vim.api.nvim_buf_get_name(source) + name = filename:match(".*/%.?(.*).http$") or filename + name = name .. "#" .. vim.b[source]._rest_nvim_count + vim.b[source]._rest_nvim_count = vim.b[source]._rest_nvim_count + 1 + end end - end - local headers = parse_headers(req_node, source, ctx) - if headers["host"] and vim.startswith(url, "/") then - local host = headers["host"][1] - if not host:match("^https?://") then - local port = host:match(":(%d%d+)$") - local protocol = "http://" - if not port or port == "443" then - protocol = "https://" - end - host = protocol .. host + local headers = parse_headers(req_node, source, ctx) + if headers["host"] and vim.startswith(url, "/") then + local host = headers["host"][1] + if not host:match("^https?://") then + local port = host:match(":(%d%d+)$") + local protocol = "http://" + if not port or port == "443" then + protocol = "https://" + end + host = protocol .. host + end + url = host .. url + table.remove(headers["host"], 1) end - url = host..url - table.remove(headers["host"], 1) - end - ---@type string? - local content_type - if headers["content-type"] and #headers["content-type"] > 0 then - content_type = headers["content-type"][1]:match("([^;]+)") - end - local body - local body_node = req_node:field("body")[1] - if body_node then - body = parser.parse_body(content_type, body_node, source, ctx) - if not body then - logger.error("parsing body failed") - vim.notify("parsing request body failed. See `:Rest logs` for more info.", vim.log.levels.ERROR, { title = "rest.nvim" }) - return nil + ---@type string? + local content_type + if headers["content-type"] and #headers["content-type"] > 0 then + content_type = headers["content-type"][1]:match("([^;]+)") + end + local body + local body_node = req_node:field("body")[1] + if body_node then + body = parser.parse_body(content_type, body_node, source, ctx) + if not body then + logger.error("parsing body failed") + vim.notify( + "parsing request body failed. See `:Rest logs` for more info.", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return nil + end end - end - ---@type rest.Request - local req = { - name = name, - method = method, - url = url, - http_version = get_node_field_text(req_node, "version", source), - headers = headers, - cookies = {}, - body = body, - handlers = handlers, - } - ctx:clear_local() - jar.load_cookies(req) - return req + ---@type rest.Request + local req = { + name = name, + method = method, + url = url, + http_version = get_node_field_text(req_node, "version", source), + headers = headers, + cookies = {}, + body = body, + handlers = handlers, + } + ctx:clear_local() + jar.load_cookies(req) + return req end return parser diff --git a/lua/rest-nvim/request.lua b/lua/rest-nvim/request.lua index 5d386f8..f376128 100644 --- a/lua/rest-nvim/request.lua +++ b/lua/rest-nvim/request.lua @@ -3,12 +3,12 @@ local M = {} local parser = require("rest-nvim.parser") -local utils = require("rest-nvim.utils") +local utils = require("rest-nvim.utils") local logger = require("rest-nvim.logger") local config = require("rest-nvim.config") -local ui = require("rest-nvim.ui.result") -local nio = require("nio") -local jar = require("rest-nvim.cookie_jar") +local ui = require("rest-nvim.ui.result") +local nio = require("nio") +local jar = require("rest-nvim.cookie_jar") local clients = require("rest-nvim.client") local Context = require("rest-nvim.context").Context @@ -42,136 +42,154 @@ local rest_nvim_last_request = nil ---@param req rest.Request local function run_request(req) - logger.debug("run_request") - local client = clients.get_available_clients(req)[1] - if not client then - logger.error("can't find registered client available for request:\n" .. vim.inspect(req)) - vim.notify("Can't find registered client available for request", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - rest_nvim_last_request = req - - _G.rest_request = req - vim.api.nvim_exec_autocmds("User", { - pattern = { "RestRequest", "RestRequestPre" }, - }) - _G.rest_request = nil - - ui.update({request=req}) - - nio.run(function () - local ok, res = pcall(client.request(req).wait) - if not ok then - logger.error("request failed") - vim.notify("request failed", vim.log.levels.ERROR, { title = "rest.nvim" }) - return + logger.debug("run_request") + local client = clients.get_available_clients(req)[1] + if not client then + logger.error("can't find registered client available for request:\n" .. vim.inspect(req)) + vim.notify("Can't find registered client available for request", vim.log.levels.ERROR, { title = "rest.nvim" }) + return end - ---@cast res rest.Response - logger.info("request success") - - -- NOTE: wrap with schedule to do vim stuffs outside of lua callback loop (`on_exit` - -- callback from `vim.system()` call) - vim.schedule(function () - -- run request handler scripts - vim.iter(req.handlers):each(function (f) f(res) end) - logger.info("handler done") - - _G.rest_request = req - _G.rest_response = res - vim.api.nvim_exec_autocmds("User", { - pattern = { "RestResponse", "RestResponsePre" }, - }) - _G.rest_request = nil - _G.rest_response = nil - - -- update cookie jar - jar.update_jar(req.url, res) - - -- update result UI - ui.update({response = res}) + rest_nvim_last_request = req + + _G.rest_request = req + vim.api.nvim_exec_autocmds("User", { + pattern = { "RestRequest", "RestRequestPre" }, + }) + _G.rest_request = nil + + ui.update({ request = req }) + + nio.run(function() + local ok, res = pcall(client.request(req).wait) + if not ok then + logger.error("request failed") + vim.notify("request failed", vim.log.levels.ERROR, { title = "rest.nvim" }) + return + end + ---@cast res rest.Response + logger.info("request success") + + -- NOTE: wrap with schedule to do vim stuffs outside of lua callback loop (`on_exit` + -- callback from `vim.system()` call) + vim.schedule(function() + -- run request handler scripts + vim.iter(req.handlers):each(function(f) + f(res) + end) + logger.info("handler done") + + _G.rest_request = req + _G.rest_response = res + vim.api.nvim_exec_autocmds("User", { + pattern = { "RestResponse", "RestResponsePre" }, + }) + _G.rest_request = nil + _G.rest_response = nil + + -- update cookie jar + jar.update_jar(req.url, res) + + -- update result UI + ui.update({ response = res }) + end) end) - end) - -- FIXME: return future to pass the command state + -- FIXME: return future to pass the command state end ---run request in current cursor position function M.run() - logger.info("starting request") - local req_node = parser.get_cursor_request_node() - if not req_node then - logger.error("Failed to find request at cursor position") - vim.notify("Failed to find request at cursor position. See `:Rest logs` for more info.", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local ctx = Context:new() - if config.env.enable and vim.b._rest_nvim_env_file then - ctx:load_file(vim.b._rest_nvim_env_file) - end - local req = parser.parse(req_node, 0, ctx) - if not req then - logger.error("failed to parse request") - vim.notify("failed to parse request", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local highlight = config.highlight - if highlight.enable then - utils.ts_highlight_node(0, req_node, require("rest-nvim.api").namespace) - end - run_request(req) + logger.info("starting request") + local req_node = parser.get_cursor_request_node() + if not req_node then + logger.error("Failed to find request at cursor position") + vim.notify( + "Failed to find request at cursor position. See `:Rest logs` for more info.", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + local ctx = Context:new() + if config.env.enable and vim.b._rest_nvim_env_file then + ctx:load_file(vim.b._rest_nvim_env_file) + end + local req = parser.parse(req_node, 0, ctx) + if not req then + logger.error("failed to parse request") + vim.notify("failed to parse request", vim.log.levels.ERROR, { title = "rest.nvim" }) + return + end + local highlight = config.highlight + if highlight.enable then + utils.ts_highlight_node(0, req_node, require("rest-nvim.api").namespace) + end + run_request(req) end ---@param name string function M.run_by_name(name) - local req_node = parser.get_request_node_by_name(name) - if not req_node then - logger.error("Failed to find request by name: " .. name) - vim.notify("Failed to find request by name: " .. name .. ". See `:Rest logs` for more info.", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local ctx = Context:new() - if config.env.enable and vim.b._rest_nvim_env_file then - ctx:load_file(vim.b._rest_nvim_env_file) - end - local req = parser.parse(req_node, 0, ctx) - if not req then - logger.error("failed to parse request") - vim.notify("failed to parse request", vim.log.levels.ERROR, { title = "rest.nvim" }) - return - end - local highlight = config.highlight - if highlight.enable then - utils.ts_highlight_node(0, req_node, require("rest-nvim.api").namespace) - end - run_request(req) + local req_node = parser.get_request_node_by_name(name) + if not req_node then + logger.error("Failed to find request by name: " .. name) + vim.notify( + "Failed to find request by name: " .. name .. ". See `:Rest logs` for more info.", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end + local ctx = Context:new() + if config.env.enable and vim.b._rest_nvim_env_file then + ctx:load_file(vim.b._rest_nvim_env_file) + end + local req = parser.parse(req_node, 0, ctx) + if not req then + logger.error("failed to parse request") + vim.notify("failed to parse request", vim.log.levels.ERROR, { title = "rest.nvim" }) + return + end + local highlight = config.highlight + if highlight.enable then + utils.ts_highlight_node(0, req_node, require("rest-nvim.api").namespace) + end + run_request(req) end ---run last request function M.run_last() - local req = rest_nvim_last_request - if not req then - vim.notify("No last request found", vim.log.levels.WARN, { title = "rest.nvim" }) - return false - end - run_request(req) + local req = rest_nvim_last_request + if not req then + vim.notify("No last request found", vim.log.levels.WARN, { title = "rest.nvim" }) + return false + end + run_request(req) end ---run all requests in current file with same context function M.run_all() - local reqs = parser.get_all_request_nodes(0) - local ctx = Context:new() - for _, req_node in ipairs(reqs) do - local req = parser.parse(req_node, 0, ctx) - if not req then - vim.notify("Parsing request failed. See `:Rest logs` for more info", vim.log.levels.ERROR, { title = "rest.nvim" }) - return false - end - -- FIXME: wait for previous request ends - local ok = run_request(req) - if not ok then - vim.notify("Running request failed. See `:Rest logs` for more info", vim.log.levels.ERROR, { title = "rest.nvim" }) - return + local reqs = parser.get_all_request_nodes(0) + local ctx = Context:new() + for _, req_node in ipairs(reqs) do + local req = parser.parse(req_node, 0, ctx) + if not req then + vim.notify( + "Parsing request failed. See `:Rest logs` for more info", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return false + end + -- FIXME: wait for previous request ends + local ok = run_request(req) + if not ok then + vim.notify( + "Running request failed. See `:Rest logs` for more info", + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + return + end end - end end return M diff --git a/lua/rest-nvim/script/init.lua b/lua/rest-nvim/script/init.lua index b681a87..8e134f4 100644 --- a/lua/rest-nvim/script/init.lua +++ b/lua/rest-nvim/script/init.lua @@ -13,18 +13,18 @@ local script = {} ---@param str string ---@param ctx rest.Context function script.load_pre_req_hook(str, ctx) - return function () - vim.print(str, ctx) - end + return function() + vim.print(str, ctx) + end end ---@param str string ---@param ctx rest.Context function script.load_post_req_hook(str, ctx) - ---@param res rest.Response - return function (res) - vim.print(str, ctx, res) - end + ---@param res rest.Response + return function(res) + vim.print(str, ctx, res) + end end return script diff --git a/lua/rest-nvim/script/lua.lua b/lua/rest-nvim/script/lua.lua index 1af5f2b..91d4353 100644 --- a/lua/rest-nvim/script/lua.lua +++ b/lua/rest-nvim/script/lua.lua @@ -15,72 +15,72 @@ local logger = require("rest-nvim.logger") ---@param ctx rest.Context ---@return rest.PreScriptEnv local function create_prescript_env(ctx) - ---Global Environment variables passed to pre-request scripts - ---@class rest.PreScriptEnv - local env = { - ---@class rest.PreScriptEnv.Request - request = { - ---@type rest.Env.Request.Variables - variables = { - ---Set request-local variable - set = function (key, value) - ctx:set_local(key, value) - end, - ---Retrieve variable in current request scope - get = function (key) - return ctx:resolve(key) - end, - } - }, - ---Global variable `vim` you are familiar of - vim = vim - } - return env + ---Global Environment variables passed to pre-request scripts + ---@class rest.PreScriptEnv + local env = { + ---@class rest.PreScriptEnv.Request + request = { + ---@type rest.Env.Request.Variables + variables = { + ---Set request-local variable + set = function(key, value) + ctx:set_local(key, value) + end, + ---Retrieve variable in current request scope + get = function(key) + return ctx:resolve(key) + end, + }, + }, + ---Global variable `vim` you are familiar of + vim = vim, + } + return env end ---@param ctx rest.Context ---@param res rest.Response ---@return rest.HandlerEnv local function create_handler_env(ctx, res) - ---Global Environment variables passed to response handler scripts - ---@class rest.HandlerEnv - local env = { - ---@class rest.HandlerEnv.Client - client = { - ---@type rest.Env.Request.Variables - global = { - ---Set global variable (this overwrites `vim.env`) - set = function (key, value) - vim.env[key] = value - end, - ---Retrieve global variable (return empty string if variable doesn't exist) - get = function (key) - return vim.env[key] or "" - end, - }, - }, - ---@class rest.HandlerEnv.Request - request = { - ---@type rest.Env.Request.Variables - variables = { - ---Set request-local variable - set = function (key, value) - ctx:set_local(key, value) - end, - ---Retrieve variable in current request scope - get = function (key) - return ctx:resolve(key) - end, - } - }, - -- TODO: create wrapper class for response - ---Raw response object - ---@type rest.Response - response = res, - ---Global variable `vim` you are familiar of - vim = vim - } - return env + ---Global Environment variables passed to response handler scripts + ---@class rest.HandlerEnv + local env = { + ---@class rest.HandlerEnv.Client + client = { + ---@type rest.Env.Request.Variables + global = { + ---Set global variable (this overwrites `vim.env`) + set = function(key, value) + vim.env[key] = value + end, + ---Retrieve global variable (return empty string if variable doesn't exist) + get = function(key) + return vim.env[key] or "" + end, + }, + }, + ---@class rest.HandlerEnv.Request + request = { + ---@type rest.Env.Request.Variables + variables = { + ---Set request-local variable + set = function(key, value) + ctx:set_local(key, value) + end, + ---Retrieve variable in current request scope + get = function(key) + return ctx:resolve(key) + end, + }, + }, + -- TODO: create wrapper class for response + ---Raw response object + ---@type rest.Response + response = res, + ---Global variable `vim` you are familiar of + vim = vim, + } + return env end ---@package @@ -88,27 +88,27 @@ end ---@param env table ---@return function local function load_lua(s, env) - local f, error_msg = load(s, "script_variable", "bt", env) - if error_msg then - logger.error(error_msg) - end - return assert(f) + local f, error_msg = load(s, "script_variable", "bt", env) + if error_msg then + logger.error(error_msg) + end + return assert(f) end ---@param s string ---@param ctx rest.Context ---@return function function script.load_pre_req_hook(s, ctx) - return load_lua(s, create_prescript_env(ctx)) + return load_lua(s, create_prescript_env(ctx)) end ---@param s string ---@param ctx rest.Context ---@return function function script.load_post_req_hook(s, ctx) - return function (res) - return load_lua(s, create_handler_env(ctx, res))() - end + return function(res) + return load_lua(s, create_handler_env(ctx, res))() + end end return script diff --git a/lua/rest-nvim/ui/help.lua b/lua/rest-nvim/ui/help.lua index fb10c26..9ac5906 100644 --- a/lua/rest-nvim/ui/help.lua +++ b/lua/rest-nvim/ui/help.lua @@ -12,102 +12,102 @@ local logger = require("rest-nvim.logger") local config = require("rest-nvim.config") local function set_lines(buffer, lines) - vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) end ---Get or create a new request window help buffer local function get_or_create_buf() - local tmp_name = "rest_winbar_help" - local existing_buf, help_bufnr = false, nil - - -- Check if the help buffer is already loaded - for _, id in ipairs(vim.api.nvim_list_bufs()) do - if vim.api.nvim_buf_get_name(id):find(tmp_name) then - existing_buf = true - help_bufnr = id + local tmp_name = "rest_winbar_help" + local existing_buf, help_bufnr = false, nil + + -- Check if the help buffer is already loaded + for _, id in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_get_name(id):find(tmp_name) then + existing_buf = true + help_bufnr = id + end + end + + if not existing_buf then + -- Create a new buffer + local new_bufnr = vim.api.nvim_create_buf(false, true) + local keybinds = config.ui.keybinds + vim.api.nvim_buf_set_name(new_bufnr, tmp_name) + vim.api.nvim_set_option_value("ft", "markdown", { buf = new_bufnr }) + vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_bufnr }) + vim.keymap.set("n", "q", help.close, { + desc = "Close rest.nvim request results help window", + buffer = new_bufnr, + }) + + -- Write to buffer + local buf_content = { + "**`rest.nvim` results window help**", + "", + "**Keybinds**:", + " - `" .. keybinds.prev .. "`: go to previous pane", + " - `" .. keybinds.next .. "`: go to next pane", + " - `q`: close results window", + "", + "**Press `q` to close this help window**", + } + set_lines(new_bufnr, buf_content) + + return new_bufnr end - end - - if not existing_buf then - -- Create a new buffer - local new_bufnr = vim.api.nvim_create_buf(false, true) - local keybinds = config.ui.keybinds - vim.api.nvim_buf_set_name(new_bufnr, tmp_name) - vim.api.nvim_set_option_value("ft", "markdown", { buf = new_bufnr }) - vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_bufnr }) - vim.keymap.set("n", "q", help.close, { - desc = "Close rest.nvim request results help window", - buffer = new_bufnr, - }) - -- Write to buffer - local buf_content = { - "**`rest.nvim` results window help**", - "", - "**Keybinds**:", - " - `" .. keybinds.prev .. "`: go to previous pane", - " - `" .. keybinds.next .. "`: go to next pane", - " - `q`: close results window", - "", - "**Press `q` to close this help window**", - } - set_lines(new_bufnr, buf_content) - - return new_bufnr - end - - return help_bufnr + return help_bufnr end ---Open the request results help window function help.open() - local help_bufnr = get_or_create_buf() - - -- Get the results buffer window ID - local winnr = vim.api.nvim_get_current_win() - - -- Help window sizing and positioning - local width = math.floor(vim.api.nvim_win_get_width(winnr) / 2) - local height = 8 - - local col = vim.api.nvim_win_get_width(winnr) - width - 4 - local row = vim.api.nvim_win_get_height(winnr) - height - 4 - - -- Display the help buffer window - ---@cast help_bufnr number - local help_win = vim.api.nvim_open_win(help_bufnr, true, { - style = "minimal", - border = "single", - win = winnr, - relative = "win", - width = width, - height = height, - row = row, - col = col, - }) - - -- Always conceal the markdown content - vim.api.nvim_set_option_value("conceallevel", 2, { win = help_win }) - vim.api.nvim_set_option_value("concealcursor", "nc", { win = help_win }) + local help_bufnr = get_or_create_buf() + + -- Get the results buffer window ID + local winnr = vim.api.nvim_get_current_win() + + -- Help window sizing and positioning + local width = math.floor(vim.api.nvim_win_get_width(winnr) / 2) + local height = 8 + + local col = vim.api.nvim_win_get_width(winnr) - width - 4 + local row = vim.api.nvim_win_get_height(winnr) - height - 4 + + -- Display the help buffer window + ---@cast help_bufnr number + local help_win = vim.api.nvim_open_win(help_bufnr, true, { + style = "minimal", + border = "single", + win = winnr, + relative = "win", + width = width, + height = height, + row = row, + col = col, + }) + + -- Always conceal the markdown content + vim.api.nvim_set_option_value("conceallevel", 2, { win = help_win }) + vim.api.nvim_set_option_value("concealcursor", "nc", { win = help_win }) end ---Close the request results help window function help.close() - -- Get the help buffer ID - local winnr - for _, id in ipairs(vim.api.nvim_list_wins()) do - if vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(id)):find("rest_winbar_help") then - winnr = id + -- Get the help buffer ID + local winnr + for _, id in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(id)):find("rest_winbar_help") then + winnr = id + end end - end - if not winnr then - ---@diagnostic disable-next-line need-check-nil - logger.error("Could not find a help window to close") - return - end + if not winnr then + ---@diagnostic disable-next-line need-check-nil + logger.error("Could not find a help window to close") + return + end - vim.api.nvim_win_close(winnr, false) + vim.api.nvim_win_close(winnr, false) end return help diff --git a/lua/rest-nvim/ui/panes.lua b/lua/rest-nvim/ui/panes.lua index e3d1f00..bec593e 100644 --- a/lua/rest-nvim/ui/panes.lua +++ b/lua/rest-nvim/ui/panes.lua @@ -17,26 +17,26 @@ local RestUIPaneGroup = {} ---@param direction number function RestUIPaneGroup:cycle(direction) - for index, pane in ipairs(self.panes) do - if pane.bufnr == vim.api.nvim_get_current_buf() then - local next_index = (index + direction - 1) % #self.panes + 1 - vim.api.nvim_win_set_buf(0, self.panes[next_index].bufnr) - return + for index, pane in ipairs(self.panes) do + if pane.bufnr == vim.api.nvim_get_current_buf() then + local next_index = (index + direction - 1) % #self.panes + 1 + vim.api.nvim_win_set_buf(0, self.panes[next_index].bufnr) + return + end end - end - vim.notify("`cycle()` can only be called inside the pane buffer", vim.log.levels.WARN, { title = "rest.nvim" }) + vim.notify("`cycle()` can only be called inside the pane buffer", vim.log.levels.WARN, { title = "rest.nvim" }) end function RestUIPaneGroup:render() - for _, pane in ipairs(self.panes) do - pane:render() - end + for _, pane in ipairs(self.panes) do + pane:render() + end end ---@param winnr integer function RestUIPaneGroup:enter(winnr) - if not self.panes[1].bufnr or not vim.api.nvim_buf_is_loaded(self.panes[1].bufnr) then - self:render() - end - vim.api.nvim_win_set_buf(winnr, self.panes[1].bufnr) + if not self.panes[1].bufnr or not vim.api.nvim_buf_is_loaded(self.panes[1].bufnr) then + self:render() + end + vim.api.nvim_win_set_buf(winnr, self.panes[1].bufnr) end ---@class rest.ui.panes.PaneGroupOpts @@ -52,66 +52,66 @@ local groups = {} ---@param opts? rest.ui.panes.PaneGroupOpts ---@return rest.ui.panes.PaneGroup function M.create_pane_group(name, pane_opts, opts) - ---@type rest.ui.panes.PaneGroup - local group = { name = name, panes = {} } - setmetatable(group, { __index = RestUIPaneGroup }) - if groups[name] then - error(("Pane group name '%s' is already taken"):format(name)) - end - groups[name] = group - for _, pane_opt in ipairs(pane_opts) do - ---@type rest.ui.panes.Pane - ---@diagnostic disable-next-line: missing-fields - local pane = { - name = pane_opt.name, - group = group, - render = function (self) - if not self.bufnr or not vim.api.nvim_buf_is_loaded(self.bufnr) then - self.bufnr = self.bufnr or vim.api.nvim_create_buf(false, false) - -- small trick to ensure buffer is loaded before the `BufWinEnter` event - -- unless lazy-setting winbar won't work - vim.fn.bufload(self.bufnr) - vim.bo[self.bufnr].swapfile = false - vim.b[self.bufnr].__pane_group = name - vim.api.nvim_buf_set_name(self.bufnr, name .. "#" .. self.name) - if opts and opts.on_init then - opts.on_init(self) - end - if pane_opt.on_init then - pane_opt.on_init(self) - end - end - vim.bo[self.bufnr].modifiable = true - local modifiable = pane_opt.render(self) or false - if not modifiable then - vim.bo[self.bufnr].undolevels = -1 - else - vim.bo[self.bufnr].undolevels = vim.o.undolevels - end - vim.bo[self.bufnr].modifiable = modifiable - vim.bo[self.bufnr].modified = false - end - } - table.insert(group.panes, pane) - end - return group + ---@type rest.ui.panes.PaneGroup + local group = { name = name, panes = {} } + setmetatable(group, { __index = RestUIPaneGroup }) + if groups[name] then + error(("Pane group name '%s' is already taken"):format(name)) + end + groups[name] = group + for _, pane_opt in ipairs(pane_opts) do + ---@type rest.ui.panes.Pane + ---@diagnostic disable-next-line: missing-fields + local pane = { + name = pane_opt.name, + group = group, + render = function(self) + if not self.bufnr or not vim.api.nvim_buf_is_loaded(self.bufnr) then + self.bufnr = self.bufnr or vim.api.nvim_create_buf(false, false) + -- small trick to ensure buffer is loaded before the `BufWinEnter` event + -- unless lazy-setting winbar won't work + vim.fn.bufload(self.bufnr) + vim.bo[self.bufnr].swapfile = false + vim.b[self.bufnr].__pane_group = name + vim.api.nvim_buf_set_name(self.bufnr, name .. "#" .. self.name) + if opts and opts.on_init then + opts.on_init(self) + end + if pane_opt.on_init then + pane_opt.on_init(self) + end + end + vim.bo[self.bufnr].modifiable = true + local modifiable = pane_opt.render(self) or false + if not modifiable then + vim.bo[self.bufnr].undolevels = -1 + else + vim.bo[self.bufnr].undolevels = vim.o.undolevels + end + vim.bo[self.bufnr].modifiable = modifiable + vim.bo[self.bufnr].modified = false + end, + } + table.insert(group.panes, pane) + end + return group end ---@return string function M.winbar() - local group = groups[vim.b.__pane_group] - if not group then - return "not a pane buffer" - end - local winbar = {} - for _, pane in ipairs(group.panes) do - if pane.bufnr == vim.api.nvim_get_current_buf() then - table.insert(winbar, "%#RestPaneTitle#" .. pane.name .. "%#Normal#") - else - table.insert(winbar, "%#RestPaneTitleNC#" .. pane.name .. "%#Normal#") + local group = groups[vim.b.__pane_group] + if not group then + return "not a pane buffer" + end + local winbar = {} + for _, pane in ipairs(group.panes) do + if pane.bufnr == vim.api.nvim_get_current_buf() then + table.insert(winbar, "%#RestPaneTitle#" .. pane.name .. "%#Normal#") + else + table.insert(winbar, "%#RestPaneTitleNC#" .. pane.name .. "%#Normal#") + end end - end - return table.concat(winbar, " %#RestText#|%#Normal# ") + return table.concat(winbar, " %#RestText#|%#Normal# ") end return M diff --git a/lua/rest-nvim/ui/result.lua b/lua/rest-nvim/ui/result.lua index 742a1cd..c5c6ae5 100644 --- a/lua/rest-nvim/ui/result.lua +++ b/lua/rest-nvim/ui/result.lua @@ -14,146 +14,160 @@ local paneui = require("rest-nvim.ui.panes") local logger = require("rest-nvim.logger") local function set_lines(buffer, lines) - vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) end ---@param buffer integer ---@param filetype string local function syntax_highlight(buffer, filetype) - local lang = vim.treesitter.language.get_lang(filetype) - local ok = pcall(vim.treesitter.start, buffer, lang) - if not ok then - vim.bo[buffer].syntax = filetype - end + local lang = vim.treesitter.language.get_lang(filetype) + local ok = pcall(vim.treesitter.start, buffer, lang) + if not ok then + vim.bo[buffer].syntax = filetype + end end ---data used to render the UI ---@class rest.UIData local data = { - ---@type rest.Request? - request = nil, - ---@type rest.Response? - response = nil + ---@type rest.Request? + request = nil, + ---@type rest.Response? + response = nil, } ---@param req rest.Request ---@return string[] local function render_request(req) - local req_line = req.method .. " " .. req.url - if req.http_version then - req_line = req_line .. " " .. req.http_version - end - return { - "### " .. req.name, - req_line, - } + local req_line = req.method .. " " .. req.url + if req.http_version then + req_line = req_line .. " " .. req.http_version + end + return { + "### " .. req.name, + req_line, + } end ---@type rest.ui.panes.PaneOpts[] local panes = { - { - name = "Response", - render = function(self) - if not data.request then - vim.bo[self.bufnr].undolevels = -1 - set_lines(self.bufnr, { "No Request running" }) - return - end - -- HACK: `vim.treesitter.foldexpr()` finds fold based on filetype not registered parser of - -- current buffer - vim.bo[self.bufnr].filetype = "http" - vim.b[self.bufnr].__rest_no_http_file = true - -- syntax_highlight(self.bufnr, "http") - local lines = render_request(data.request) - if data.response then - logger.debug(data.response.status) - table.insert(lines, ("%s %d %s"):format(data.response.status.version, data.response.status.code, data.response.status.text)) - local content_type = data.response.headers["content-type"] - table.insert(lines, "") - table.insert(lines, "# @_RES") - local body = vim.split(data.response.body, "\n") - if content_type then - local base_type, res_type = content_type[1]:match("(.*)/([^;]+)") - if base_type == "image" then - body = { "Binary(image) answer" } - elseif res_type == "octet_stream" then - body = { "Binary answer" } - elseif config.response.hooks.format then - -- NOTE: format hook runs here because it should be done last. - body = utils.gq_lines(body, res_type) - end - end - vim.list_extend(lines, body) - table.insert(lines, "# @_END") - else - vim.list_extend(lines, { "", "# Loading..." }) - end - set_lines(self.bufnr, lines) - return false - end, - }, - { - name = "Headers", - render = function(self) - if not data.response then - set_lines(self.bufnr, { "Loading..." }) - return - end - syntax_highlight(self.bufnr, "jproperties") - local lines = {} - logger.debug(data.response.headers) - local headers = vim.iter(data.response.headers):totable() - table.sort(headers, function(b, a) return a[1] > b[1] end) - logger.debug(headers) - for _, header in ipairs(headers) do - if header[1] ~= "set-cookie" then - vim.list_extend(lines, vim.iter(header[2]):map(function (value) - return header[1] .. ": " .. value - end):totable()) - end - end - set_lines(self.bufnr, lines) - end, - }, - { - name = "Cookies", - render = function(self) - if not data.response then - set_lines(self.bufnr, { "Loading..." }) - return - end - local lines = {} - ---@type string[]? - local cookie_headers = vim.tbl_get(data.response, "headers", "set-cookie") - if not cookie_headers then - set_lines(self.bufnr, { "No Cookies" }) - return - end - syntax_highlight(self.bufnr, "jproperties") - table.sort(cookie_headers) - vim.list_extend(lines, cookie_headers) - set_lines(self.bufnr, lines) - end, - }, - { - name = "Statistics", - render = function(self) - if not data.response then - set_lines(self.bufnr, { "Loading..." }) - return - end - local lines = {} - if not data.response.statistics then - set_lines(self.bufnr, { "No Statistics" }) - return - end - syntax_highlight(self.bufnr, "jproperties") - for key, value in pairs(data.response.statistics) do - table.insert(lines, ("%s: %s"):format(key, value)) - end - set_lines(self.bufnr, lines) - end, - }, + { + name = "Response", + render = function(self) + if not data.request then + vim.bo[self.bufnr].undolevels = -1 + set_lines(self.bufnr, { "No Request running" }) + return + end + -- HACK: `vim.treesitter.foldexpr()` finds fold based on filetype not registered parser of + -- current buffer + vim.bo[self.bufnr].filetype = "http" + vim.b[self.bufnr].__rest_no_http_file = true + -- syntax_highlight(self.bufnr, "http") + local lines = render_request(data.request) + if data.response then + logger.debug(data.response.status) + table.insert( + lines, + ("%s %d %s"):format( + data.response.status.version, + data.response.status.code, + data.response.status.text + ) + ) + local content_type = data.response.headers["content-type"] + table.insert(lines, "") + table.insert(lines, "# @_RES") + local body = vim.split(data.response.body, "\n") + if content_type then + local base_type, res_type = content_type[1]:match("(.*)/([^;]+)") + if base_type == "image" then + body = { "Binary(image) answer" } + elseif res_type == "octet_stream" then + body = { "Binary answer" } + elseif config.response.hooks.format then + -- NOTE: format hook runs here because it should be done last. + body = utils.gq_lines(body, res_type) + end + end + vim.list_extend(lines, body) + table.insert(lines, "# @_END") + else + vim.list_extend(lines, { "", "# Loading..." }) + end + set_lines(self.bufnr, lines) + return false + end, + }, + { + name = "Headers", + render = function(self) + if not data.response then + set_lines(self.bufnr, { "Loading..." }) + return + end + syntax_highlight(self.bufnr, "jproperties") + local lines = {} + logger.debug(data.response.headers) + local headers = vim.iter(data.response.headers):totable() + table.sort(headers, function(b, a) + return a[1] > b[1] + end) + logger.debug(headers) + for _, header in ipairs(headers) do + if header[1] ~= "set-cookie" then + vim.list_extend( + lines, + vim.iter(header[2]) + :map(function(value) + return header[1] .. ": " .. value + end) + :totable() + ) + end + end + set_lines(self.bufnr, lines) + end, + }, + { + name = "Cookies", + render = function(self) + if not data.response then + set_lines(self.bufnr, { "Loading..." }) + return + end + local lines = {} + ---@type string[]? + local cookie_headers = vim.tbl_get(data.response, "headers", "set-cookie") + if not cookie_headers then + set_lines(self.bufnr, { "No Cookies" }) + return + end + syntax_highlight(self.bufnr, "jproperties") + table.sort(cookie_headers) + vim.list_extend(lines, cookie_headers) + set_lines(self.bufnr, lines) + end, + }, + { + name = "Statistics", + render = function(self) + if not data.response then + set_lines(self.bufnr, { "Loading..." }) + return + end + local lines = {} + if not data.response.statistics then + set_lines(self.bufnr, { "No Statistics" }) + return + end + syntax_highlight(self.bufnr, "jproperties") + for key, value in pairs(data.response.statistics) do + table.insert(lines, ("%s: %s"):format(key, value)) + end + set_lines(self.bufnr, lines) + end, + }, } local winbar = "%#Normal# %{%v:lua.require('rest-nvim.ui.panes').winbar()%}" @@ -165,84 +179,84 @@ winbar = winbar .. "%#RestText#Press %#Keyword#?%#RestText# for help%#Normal# " ---Winbar component showing response statistics ---@return string function ui.stat_winbar() - local content = "" - if not data.response then - return "Loading...%#Normal#" - end - for stat_name, stat_value in pairs(data.response.statistics) do - local style = config.clients.curl.statistics[stat_name] or {} - if style.winbar then - local title = type(style.winbar) == "string" and style.winbar or (style.title or stat_name):lower() - if title ~= "" then - title = title .. ": " - end - local value, representation = vim.split(stat_value, " ")[1], vim.split(stat_value, " ")[2] - content = content .. " %#RestText#" .. title .. "%#Number#" .. value .. " %#Normal#" .. representation + local content = "" + if not data.response then + return "Loading...%#Normal#" + end + for stat_name, stat_value in pairs(data.response.statistics) do + local style = config.clients.curl.statistics[stat_name] or {} + if style.winbar then + local title = type(style.winbar) == "string" and style.winbar or (style.title or stat_name):lower() + if title ~= "" then + title = title .. ": " + end + local value, representation = vim.split(stat_value, " ")[1], vim.split(stat_value, " ")[2] + content = content .. " %#RestText#" .. title .. "%#Number#" .. value .. " %#Normal#" .. representation + end end - end - return content + return content end ---@type rest.ui.panes.PaneGroup local group = paneui.create_pane_group("rest_nvim_result", panes, { - on_init = function(self) - local help = require("rest-nvim.ui.help") - vim.keymap.set("n", config.ui.keybinds.prev, function() - self.group:cycle(-1) - end, { buffer = self.bufnr }) - vim.keymap.set("n", config.ui.keybinds.next, function() - self.group:cycle(1) - end, { buffer = self.bufnr }) - vim.keymap.set("n", "?", help.open, { buffer = self.bufnr }) - vim.bo[self.bufnr].filetype = "rest_nvim_result" - if config.ui.winbar then - utils.nvim_lazy_set_wo(self.bufnr, "winbar", winbar) - end - end, + on_init = function(self) + local help = require("rest-nvim.ui.help") + vim.keymap.set("n", config.ui.keybinds.prev, function() + self.group:cycle(-1) + end, { buffer = self.bufnr }) + vim.keymap.set("n", config.ui.keybinds.next, function() + self.group:cycle(1) + end, { buffer = self.bufnr }) + vim.keymap.set("n", "?", help.open, { buffer = self.bufnr }) + vim.bo[self.bufnr].filetype = "rest_nvim_result" + if config.ui.winbar then + utils.nvim_lazy_set_wo(self.bufnr, "winbar", winbar) + end + end, }) ---Get the foreground value of a highlighting group ---@param name string Highlighting group name ---@return string local function get_hl_group_fg(name) - -- This will still error out if the highlight doesn't exist - return string.format("#%06X", vim.api.nvim_get_hl(0, { name = name, link = false }).fg) + -- This will still error out if the highlight doesn't exist + return string.format("#%06X", vim.api.nvim_get_hl(0, { name = name, link = false }).fg) end vim.api.nvim_set_hl(0, "RestText", { fg = get_hl_group_fg("Comment") }) vim.api.nvim_set_hl(0, "RestPaneTitleNC", { fg = get_hl_group_fg("Statement") }) vim.api.nvim_set_hl(0, "RestPaneTitle", { - fg = get_hl_group_fg("Statement"), - bold = true, - underline = true, + fg = get_hl_group_fg("Statement"), + bold = true, + underline = true, }) ---Check if UI window is shown in current tabpage ---@return boolean function ui.is_open() - local winnr = vim.iter(vim.api.nvim_tabpage_list_wins(0)):find(function(id) - local buf = vim.api.nvim_win_get_buf(id) - return vim.b[buf].__pane_group == group.name - end) - return winnr ~= nil + local winnr = vim.iter(vim.api.nvim_tabpage_list_wins(0)):find(function(id) + local buf = vim.api.nvim_win_get_buf(id) + return vim.b[buf].__pane_group == group.name + end) + return winnr ~= nil end ---@param winnr integer function ui.enter(winnr) - group:enter(winnr) + group:enter(winnr) end ---Clear the UI function ui.clear() - data = {} - group:render() + data = {} + group:render() end ---Update data and rerender the UI ---@param new_data rest.UIData function ui.update(new_data) - data = vim.tbl_deep_extend("force", data, new_data) - group:render() + data = vim.tbl_deep_extend("force", data, new_data) + group:render() end return ui diff --git a/lua/rest-nvim/utils.lua b/lua/rest-nvim/utils.lua index 25bf5d1..17d4c68 100644 --- a/lua/rest-nvim/utils.lua +++ b/lua/rest-nvim/utils.lua @@ -19,141 +19,141 @@ local uv = vim.uv or vim.loop ---@param only_necessary? boolean Encode only necessary characters ---@return string function utils.escape(str, only_necessary) - local ignore = "%w%-%.%_%~%+" - if only_necessary then - ignore = ignore .. "%:%/%?%=%&%#" - end - local pattern = "([^" .. ignore .. "])" - local encoded = string.gsub(str, pattern, function(c) - if c == " " then - return "+" + local ignore = "%w%-%.%_%~%+" + if only_necessary then + ignore = ignore .. "%:%/%?%=%&%#" end - return string.format("%%%02x", string.byte(c)) - end) - - return encoded + local pattern = "([^" .. ignore .. "])" + local encoded = string.gsub(str, pattern, function(c) + if c == " " then + return "+" + end + return string.format("%%%02x", string.byte(c)) + end) + + return encoded end ---@param str string function utils.url_decode(str) - str = string.gsub(str, "%+", " ") - str = string.gsub(str, "%%(%x%x)", function(hex) - return string.char(tonumber(hex, 16)) - end) - return str + str = string.gsub(str, "%+", " ") + str = string.gsub(str, "%%(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end) + return str end ---Check if a file exists in the given `path` ---@param path string file path ---@return boolean function utils.file_exists(path) - ---@diagnostic disable-next-line undefined-field - local fd = uv.fs_open(path, "r", 438) - if fd then ---@diagnostic disable-next-line undefined-field - uv.fs_close(fd) - return true - end + local fd = uv.fs_open(path, "r", 438) + if fd then + ---@diagnostic disable-next-line undefined-field + uv.fs_close(fd) + return true + end - return false + return false end ---Read a file if it exists ---@param path string file path ---@return string function utils.read_file(path) - ---@type string|nil - local content - if utils.file_exists(path) then - ---@diagnostic disable-next-line undefined-field - local file = uv.fs_open(path, "r", 438) - ---@diagnostic disable-next-line undefined-field - local stat = uv.fs_fstat(file) - ---@diagnostic disable-next-line undefined-field - content = uv.fs_read(file, stat.size, 0) - ---@diagnostic disable-next-line undefined-field - uv.fs_close(file) - else - ---@diagnostic disable-next-line need-check-nil - logger.error("Failed to read file '" .. path .. "'") - return "" - end - - ---@cast content string - return content + ---@type string|nil + local content + if utils.file_exists(path) then + ---@diagnostic disable-next-line undefined-field + local file = uv.fs_open(path, "r", 438) + ---@diagnostic disable-next-line undefined-field + local stat = uv.fs_fstat(file) + ---@diagnostic disable-next-line undefined-field + content = uv.fs_read(file, stat.size, 0) + ---@diagnostic disable-next-line undefined-field + uv.fs_close(file) + else + ---@diagnostic disable-next-line need-check-nil + logger.error("Failed to read file '" .. path .. "'") + return "" + end + + ---@cast content string + return content end function utils.parse_http_time(time_str) - local pattern = "(%a+), (%d+) (%a+) (%d+) (%d+):(%d+):(%d+) GMT" - local _, day, month_name, year, hour, min, sec = time_str:match(pattern) + local pattern = "(%a+), (%d+) (%a+) (%d+) (%d+):(%d+):(%d+) GMT" + local _, day, month_name, year, hour, min, sec = time_str:match(pattern) -- stylua: ignore local months = { Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12, } - local time_table = { - year = tonumber(year), - month = months[month_name], - day = tonumber(day), - hour = tonumber(hour), - min = tonumber(min), - sec = tonumber(sec), - isdst = false, - } - ---@diagnostic disable-next-line: param-type-mismatch - local gmt_offset = os.difftime(os.time(), os.time(os.date("!*t"))) - return os.time(time_table) + gmt_offset + local time_table = { + year = tonumber(year), + month = months[month_name], + day = tonumber(day), + hour = tonumber(hour), + min = tonumber(min), + sec = tonumber(sec), + isdst = false, + } + ---@diagnostic disable-next-line: param-type-mismatch + local gmt_offset = os.difftime(os.time(), os.time(os.date("!*t"))) + return os.time(time_table) + gmt_offset end --- Default transformers for statistics local transform = { - ---Transform `time` into a readable typed time (e.g. 200ms) - ---@param time string - ---@return string - time = function(time) - ---@diagnostic disable-next-line cast-local-type - time = tonumber(time) + ---Transform `time` into a readable typed time (e.g. 200ms) + ---@param time string + ---@return string + time = function(time) + ---@diagnostic disable-next-line cast-local-type + time = tonumber(time) - if time >= 60 then - time = string.format("%.2f", time / 60) + if time >= 60 then + time = string.format("%.2f", time / 60) - return time .. " min" - end + return time .. " min" + end - local units = { "s", "ms", "µs", "ns" } - local unit = 1 + local units = { "s", "ms", "µs", "ns" } + local unit = 1 - while time < 1 and unit < #units do - ---@diagnostic disable-next-line cast-local-type - time = time * 1000 - unit = unit + 1 - end + while time < 1 and unit < #units do + ---@diagnostic disable-next-line cast-local-type + time = time * 1000 + unit = unit + 1 + end - time = string.format("%.2f", time) + time = string.format("%.2f", time) - return time .. " " .. units[unit] - end, + return time .. " " .. units[unit] + end, - ---Transform `bytes` into another bigger size type if needed - ---@param bytes string - ---@return string - size = function(bytes) - ---@diagnostic disable-next-line cast-local-type - bytes = tonumber(bytes) + ---Transform `bytes` into another bigger size type if needed + ---@param bytes string + ---@return string + size = function(bytes) + ---@diagnostic disable-next-line cast-local-type + bytes = tonumber(bytes) - local units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" } - local unit = 1 + local units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" } + local unit = 1 - while bytes >= 1024 and unit <= #units do - ---@diagnostic disable-next-line cast-local-type - bytes = bytes / 1024 - unit = unit + 1 - end + while bytes >= 1024 and unit <= #units do + ---@diagnostic disable-next-line cast-local-type + bytes = bytes / 1024 + unit = unit + 1 + end - bytes = string.format("%.2f", bytes) + bytes = string.format("%.2f", bytes) - return bytes .. " " .. units[unit] - end, + return bytes .. " " .. units[unit] + end, } utils.transform_time = transform.time @@ -163,51 +163,44 @@ utils.transform_size = transform.size ---@param node TSNode ---@param ns number function utils.ts_highlight_node(bufnr, node, ns) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - local highlight = require("rest-nvim.config").highlight - local higroup = "IncSearch" - local s_row, s_col = node:start() - local e_row, e_col = node:end_() - -- don't try to highlight over the last line - if e_col == 0 then - e_row = e_row - 1 - e_col = -1 - end - vim.highlight.range( - bufnr, - ns, - higroup, - { s_row, s_col }, - { e_row, e_col }, - { regtype = "v" } - ) - - -- Clear buffer highlights again after timeout - vim.defer_fn(function() - if vim.api.nvim_buf_is_valid(bufnr) then - vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() end - end, highlight.timeout) + local highlight = require("rest-nvim.config").highlight + local higroup = "IncSearch" + local s_row, s_col = node:start() + local e_row, e_col = node:end_() + -- don't try to highlight over the last line + if e_col == 0 then + e_row = e_row - 1 + e_col = -1 + end + vim.highlight.range(bufnr, ns, higroup, { s_row, s_col }, { e_row, e_col }, { regtype = "v" }) + + -- Clear buffer highlights again after timeout + vim.defer_fn(function() + if vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + end + end, highlight.timeout) end ---@param source string|integer ---@return vim.treesitter.LanguageTree function utils.ts_get_parser(source) - if type(source) == "string" then - return vim.treesitter.get_string_parser(source, "http") - else - return vim.treesitter.get_parser(source, "http") - end + if type(source) == "string" then + return vim.treesitter.get_string_parser(source, "http") + else + return vim.treesitter.get_parser(source, "http") + end end ---@param source string|integer ---@return vim.treesitter.LanguageTree ---@return TSTree function utils.ts_parse_source(source) - local ts_parser = utils.ts_get_parser(source) - return ts_parser, assert(ts_parser:parse(false)[1]) + local ts_parser = utils.ts_get_parser(source) + return ts_parser, assert(ts_parser:parse(false)[1]) end ---@param node TSNode @@ -215,64 +208,64 @@ end ---@param oneline boolean? ---@return TSNode? function utils.ts_find(node, type, oneline) - if oneline then - local sr, _, er, ec = node:range() - local is_oneline = (sr == er) or (er - sr == 1 and ec == 0) - if not is_oneline then - return nil + if oneline then + local sr, _, er, ec = node:range() + local is_oneline = (sr == er) or (er - sr == 1 and ec == 0) + if not is_oneline then + return nil + end + end + if node:type() == type then + return node end - end - if node:type() == type then - return node - end - local parent = node:parent() - if parent then - return utils.ts_find(parent, type) - end - return nil + local parent = node:parent() + if parent then + return utils.ts_find(parent, type) + end + return nil end ---@param node TSNode function utils.ts_upper_node(node) - local start_row, _, _, _ = node:range() - local end_row = start_row - start_row = start_row - 1 - local start_col = 0 - local end_col = 0 - -- HACK: root node type might not be "document" - local root_node = assert(utils.ts_find(node, "document")) - local min_node = root_node:named_descendant_for_range(start_row, start_col, end_row, end_col) - return min_node + local start_row, _, _, _ = node:range() + local end_row = start_row + start_row = start_row - 1 + local start_col = 0 + local end_col = 0 + -- HACK: root node type might not be "document" + local root_node = assert(utils.ts_find(node, "document")) + local min_node = root_node:named_descendant_for_range(start_row, start_col, end_row, end_col) + return min_node end ---@param node TSNode ---@param expected_type string ---@return table function utils.ts_node_spec(node, expected_type) - return { - node, - function (n) - return n:type() == expected_type - end, - "("..expected_type..") TSNode", - } + return { + node, + function(n) + return n:type() == expected_type + end, + "(" .. expected_type .. ") TSNode", + } end ---Create error log for TSNode that has a syntax error ---@param node TSNode Tree-sitter node ---@return string function utils.ts_node_error_log(node) - local s_row, s_col = node:start() - local e_row, e_col = node:end_() - local range = "[" - - if s_row == e_row then - range = range .. s_row .. ":" .. s_col .. " - " .. e_col - else - range = range .. s_row .. ":" .. s_col .. " - " .. e_row .. ":" .. e_col - end - range = range .. "]" - return "The tree-sitter node at the range " .. range .. " has a syntax error and cannot be parsed" + local s_row, s_col = node:start() + local e_row, e_col = node:end_() + local range = "[" + + if s_row == e_row then + range = range .. s_row .. ":" .. s_col .. " - " .. e_col + else + range = range .. s_row .. ":" .. s_col .. " - " .. e_row .. ":" .. e_col + end + range = range .. "]" + return "The tree-sitter node at the range " .. range .. " has a syntax error and cannot be parsed" end ---Set window-option to specific buffer @@ -282,13 +275,13 @@ end ---@param name string ---@param value any function utils.nvim_lazy_set_wo(bufnr, name, value) - vim.api.nvim_create_autocmd("BufWinEnter", { - buffer = bufnr, - callback = function() - vim.api.nvim_set_option_value(name, value, { scope = "local" }) - end, - once = true, - }) + vim.api.nvim_create_autocmd("BufWinEnter", { + buffer = bufnr, + callback = function() + vim.api.nvim_set_option_value(name, value, { scope = "local" }) + end, + once = true, + }) end ---format lines using native vim `gq` command @@ -296,14 +289,14 @@ end ---@param filetype string ---@return string[] function utils.gq_lines(lines, filetype) - local format_buf = vim.api.nvim_create_buf(false, true) - vim.bo[format_buf].filetype = filetype - vim.api.nvim_buf_set_lines(format_buf, 0, -1, false, lines) - vim.api.nvim_buf_call(format_buf, function () - vim.cmd("normal gg") - vim.cmd("normal gqG") - end) - return vim.api.nvim_buf_get_lines(format_buf, 0, -1, false) + local format_buf = vim.api.nvim_create_buf(false, true) + vim.bo[format_buf].filetype = filetype + vim.api.nvim_buf_set_lines(format_buf, 0, -1, false, lines) + vim.api.nvim_buf_call(format_buf, function() + vim.cmd("normal gg") + vim.cmd("normal gqG") + end) + return vim.api.nvim_buf_get_lines(format_buf, 0, -1, false) end return utils diff --git a/lua/telescope/_extensions/rest.lua b/lua/telescope/_extensions/rest.lua index 3e22e46..ff9ca88 100644 --- a/lua/telescope/_extensions/rest.lua +++ b/lua/telescope/_extensions/rest.lua @@ -9,46 +9,46 @@ local conf = require("telescope.config").values local make_entry = require("telescope.make_entry") local function rest_env_select(_) - local dotenv = require("rest-nvim.dotenv") + local dotenv = require("rest-nvim.dotenv") - local lines = dotenv.find_env_files() + local lines = dotenv.find_env_files() - local opts = {} - opts.entry_maker = make_entry.gen_from_file(opts) + local opts = {} + opts.entry_maker = make_entry.gen_from_file(opts) - pickers - .new(opts, { - prompt_title = "Select Env File", - finder = finders.new_table({ - results = lines, - entry_maker = make_entry.gen_from_file(), - }), - attach_mappings = function(prompt_bufnr, map) - actions.select_default:replace(function() - local selection = action_state.get_selected_entry() - actions.close(prompt_bufnr) - if selection == nil then - return - end - dotenv.register_file(selection[1]) - end) - map("i", "", function() - actions.close(prompt_bufnr) - local selection = state.get_selected_entry() - if selection == nil then - return - end - vim.cmd.edit(selection[1]) - end) - return true - end, - previewer = conf.file_previewer(opts), - }) - :find() + pickers + .new(opts, { + prompt_title = "Select Env File", + finder = finders.new_table({ + results = lines, + entry_maker = make_entry.gen_from_file(), + }), + attach_mappings = function(prompt_bufnr, map) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + actions.close(prompt_bufnr) + if selection == nil then + return + end + dotenv.register_file(selection[1]) + end) + map("i", "", function() + actions.close(prompt_bufnr) + local selection = state.get_selected_entry() + if selection == nil then + return + end + vim.cmd.edit(selection[1]) + end) + return true + end, + previewer = conf.file_previewer(opts), + }) + :find() end return telescope.register_extension({ - exports = { - select_env = rest_env_select, - }, + exports = { + select_env = rest_env_select, + }, }) diff --git a/plugin/rest-nvim.lua b/plugin/rest-nvim.lua index 1b17875..1f9ca9c 100644 --- a/plugin/rest-nvim.lua +++ b/plugin/rest-nvim.lua @@ -1,11 +1,11 @@ ---@diagnostic disable: invisible if vim.fn.has("nvim-0.9.0") ~= 1 then - vim.notify_once("[rest.nvim] rest.nvim requires at least Neovim >= 0.9 in order to work") - return + vim.notify_once("[rest.nvim] rest.nvim requires at least Neovim >= 0.9 in order to work") + return end if vim.g.loaded_rest_nvim then - return + return end --- Dependencies management --- @@ -16,53 +16,57 @@ local rest_nvim_deps = {} -- Locate dependencies local dependencies = { - ["nvim-nio"] = "rest.nvim will not work asynchronously", - xml2lua = "rest.nvim will be completely unable to use XML bodies in your requests", - mimetypes = "rest.nvim will be completely unable to recognize the file type of external body files", - ["fidget.nvim"] = "rest.nvim will be completely unable to show request progress messages", + ["nvim-nio"] = "rest.nvim will not work asynchronously", + xml2lua = "rest.nvim will be completely unable to use XML bodies in your requests", + mimetypes = "rest.nvim will be completely unable to recognize the file type of external body files", + ["fidget.nvim"] = "rest.nvim will be completely unable to show request progress messages", } for dep, err in pairs(dependencies) do - local found_dep - -- Both nvim-nio and lua-curl has a different Lua module name - if dep == "nvim-nio" then - found_dep = package.searchpath("nio", package.path) - elseif dep == "fidget.nvim" then - found_dep = package.searchpath("fidget", package.path) - else - found_dep = package.searchpath(dep, package.path) - end - - -- If the dependency could not be find in the Lua package.path then try to load it using pcall - -- in case it has been installed through a regular plugin manager and not rocks.nvim - if not found_dep then - local found_dep2 + local found_dep -- Both nvim-nio and lua-curl has a different Lua module name if dep == "nvim-nio" then - found_dep2 = pcall(require, "nio") + found_dep = package.searchpath("nio", package.path) elseif dep == "fidget.nvim" then - found_dep2 = pcall(require, "fidget") + found_dep = package.searchpath("fidget", package.path) else - found_dep2 = pcall(require, dep) + found_dep = package.searchpath(dep, package.path) end - rest_nvim_deps[dep] = { - found = false, - error = err, - } - if not found_dep2 then - vim.notify("WARN: Dependency '" .. dep .. "' was not found. " .. err, vim.log.levels.ERROR, { title = "rest.nvim" }) + -- If the dependency could not be find in the Lua package.path then try to load it using pcall + -- in case it has been installed through a regular plugin manager and not rocks.nvim + if not found_dep then + local found_dep2 + -- Both nvim-nio and lua-curl has a different Lua module name + if dep == "nvim-nio" then + found_dep2 = pcall(require, "nio") + elseif dep == "fidget.nvim" then + found_dep2 = pcall(require, "fidget") + else + found_dep2 = pcall(require, dep) + end + + rest_nvim_deps[dep] = { + found = false, + error = err, + } + if not found_dep2 then + vim.notify( + "WARN: Dependency '" .. dep .. "' was not found. " .. err, + vim.log.levels.ERROR, + { title = "rest.nvim" } + ) + else + rest_nvim_deps[dep] = { + found = true, + error = err, + } + end else - rest_nvim_deps[dep] = { - found = true, - error = err, - } + rest_nvim_deps[dep] = { + found = true, + error = err, + } end - else - rest_nvim_deps[dep] = { - found = true, - error = err, - } - end end vim.g.rest_nvim_deps = rest_nvim_deps diff --git a/spec/client/curl/cli_spec.lua b/spec/client/curl/cli_spec.lua index 9cb3820..a23f655 100644 --- a/spec/client/curl/cli_spec.lua +++ b/spec/client/curl/cli_spec.lua @@ -11,117 +11,128 @@ local parser = curl.parser local STAT_FORMAT = builder.STAT_ARGS[2] describe("curl cli builder", function() - it("from GET request", function() - local args = builder.build({ - context = Context:new(), - method = "GET", - url = "http://localhost:8000", - headers = {}, - cookies = {}, - handlers = {}, - }) - assert.same({ "http://localhost:8000", "-X", "GET", "-w", STAT_FORMAT }, args) - end) - it("from GET request with headers", function() - local args = builder.build({ - context = Context:new(), - method = "GET", - url = "http://localhost:8000", - headers = { - ["x-foo"] = { "bar" }, - }, - cookies = {}, - handlers = {}, - }) - assert.same({ "http://localhost:8000", "-X", "GET", "-H", "X-Foo: bar", "-w", STAT_FORMAT }, args) - end) - it("from POST request with form body", function () - local args = builder.build({ - context = Context:new(), - method = "POST", - url = "http://localhost:8000", - headers = {}, - cookies = {}, - handlers = {}, - body = { - __TYPE = "raw", - data = "field1=value1&field2=value2", - }, - }) - assert.same({ "http://localhost:8000", "-X", "POST", "--data-raw", "field1=value1&field2=value2", "-w", STAT_FORMAT }, args) - end) - it("from POST request with json body", function () - local json_text = [[{ "string": "foo", "number": 100, "array": [1, 2, 3], "json": { "key": "value" } }]] - local args = builder.build({ - context = Context:new(), - method = "POST", - url = "http://localhost:8000", - headers = {}, - cookies = {}, - handlers = {}, - body = { - __TYPE = "json", - data = json_text, - }, - }) - assert.same({ "http://localhost:8000", "-X", "POST", "--data-raw", json_text, "-w", STAT_FORMAT }, args) - end) - it("from POST request with external body", function () - local args = builder.build({ - context = Context:new(), - method = "POST", - url = "http://localhost:8000", - headers = {}, - cookies = {}, - handlers = {}, - body = { - __TYPE = "external", - data = { - path = "spec/test_server/post_json.json" - }, - }, - }) - assert.same( - { "http://localhost:8000", "-X", "POST", "--data-binary", "@spec/test_server/post_json.json", "-w", STAT_FORMAT }, - args - ) - end) + it("from GET request", function() + local args = builder.build({ + context = Context:new(), + method = "GET", + url = "http://localhost:8000", + headers = {}, + cookies = {}, + handlers = {}, + }) + assert.same({ "http://localhost:8000", "-X", "GET", "-w", STAT_FORMAT }, args) + end) + it("from GET request with headers", function() + local args = builder.build({ + context = Context:new(), + method = "GET", + url = "http://localhost:8000", + headers = { + ["x-foo"] = { "bar" }, + }, + cookies = {}, + handlers = {}, + }) + assert.same({ "http://localhost:8000", "-X", "GET", "-H", "X-Foo: bar", "-w", STAT_FORMAT }, args) + end) + it("from POST request with form body", function() + local args = builder.build({ + context = Context:new(), + method = "POST", + url = "http://localhost:8000", + headers = {}, + cookies = {}, + handlers = {}, + body = { + __TYPE = "raw", + data = "field1=value1&field2=value2", + }, + }) + assert.same( + { "http://localhost:8000", "-X", "POST", "--data-raw", "field1=value1&field2=value2", "-w", STAT_FORMAT }, + args + ) + end) + it("from POST request with json body", function() + local json_text = [[{ "string": "foo", "number": 100, "array": [1, 2, 3], "json": { "key": "value" } }]] + local args = builder.build({ + context = Context:new(), + method = "POST", + url = "http://localhost:8000", + headers = {}, + cookies = {}, + handlers = {}, + body = { + __TYPE = "json", + data = json_text, + }, + }) + assert.same({ "http://localhost:8000", "-X", "POST", "--data-raw", json_text, "-w", STAT_FORMAT }, args) + end) + it("from POST request with external body", function() + local args = builder.build({ + context = Context:new(), + method = "POST", + url = "http://localhost:8000", + headers = {}, + cookies = {}, + handlers = {}, + body = { + __TYPE = "external", + data = { + path = "spec/test_server/post_json.json", + }, + }, + }) + assert.same( + { + "http://localhost:8000", + "-X", + "POST", + "--data-binary", + "@spec/test_server/post_json.json", + "-w", + STAT_FORMAT, + }, + args + ) + end) end) describe("curl cli response parser", function() - it("from http GET request", function() - local stdin = { - "* Trying 127.0.0.1:8000...", - "* Connected to localhost (127.0.0.1) port 8000 (#0)", - "> GET / HTTP/1.1", - "> Host: localhost:8000", - "> User-Agent: curl/7.81.0", - "> Accept: */*", - ">", - "* Mark bundle as not supporting multiuse", - "< HTTP/1.1 200 OK", - "< Content-Type: text/plain", - "< Date: Tue, 06 Aug 2024 12:22:44 GMT", - "< Content-Length: 15", - "<", - "{ [15 bytes data]", - "* Connection #0 to host localhost left intact", - } - local response = parser.parse_verbose(stdin) - assert.same({ - status = { - version = "HTTP/1.1", - code = 200, - text = "OK", - }, - statistics = {}, - headers = { - ["content-type"] = { "text/plain" }, - date = { "Tue, 06 Aug 2024 12:22:44 GMT" }, - ["content-length"] = { "15" }, - }, - }, response) - end) + it("from http GET request", function() + local stdin = { + "* Trying 127.0.0.1:8000...", + "* Connected to localhost (127.0.0.1) port 8000 (#0)", + "> GET / HTTP/1.1", + "> Host: localhost:8000", + "> User-Agent: curl/7.81.0", + "> Accept: */*", + ">", + "* Mark bundle as not supporting multiuse", + "< HTTP/1.1 200 OK", + "< Content-Type: text/plain", + "< Date: Tue, 06 Aug 2024 12:22:44 GMT", + "< Content-Length: 15", + "<", + "{ [15 bytes data]", + "* Connection #0 to host localhost left intact", + } + local response = parser.parse_verbose(stdin) + assert.same({ + status = { + version = "HTTP/1.1", + code = 200, + text = "OK", + }, + statistics = {}, + headers = { + ["content-type"] = { "text/plain" }, + date = { "Tue, 06 Aug 2024 12:22:44 GMT" }, + ["content-length"] = { "15" }, + }, + }, response) + end) end) -- -- don't run real request on test by default diff --git a/spec/client/curl/command_spec.lua b/spec/client/curl/command_spec.lua index 83ab2c0..87e444b 100644 --- a/spec/client/curl/command_spec.lua +++ b/spec/client/curl/command_spec.lua @@ -3,26 +3,26 @@ require("spec.minimum_init") local function open(path) - vim.cmd.edit(path) - -- FIXME: why runtimepath is not working? - vim.bo.filetype = "http" - vim.cmd.source("ftplugin/http.lua") - return 0 + vim.cmd.edit(path) + -- FIXME: why runtimepath is not working? + vim.bo.filetype = "http" + vim.cmd.source("ftplugin/http.lua") + return 0 end vim.cmd.source("plugin/rest-nvim.lua") ---@diagnostic disable-next-line: duplicate-set-field -vim.notify = function () end +vim.notify = function() end -describe(":Rest curl", function () - assert(vim.g.loaded_rest_nvim) - it("yank cursor position", function () - open("spec/examples/basic_get.http") - vim.cmd("Rest curl yank") - assert.same( - "curl -sL 'https://api.github.com/users/boltlessengineer' '-X' 'GET' '-H' 'User-Agent: neovim'", - vim.fn.getreg("+") - ) - end) +describe(":Rest curl", function() + assert(vim.g.loaded_rest_nvim) + it("yank cursor position", function() + open("spec/examples/basic_get.http") + vim.cmd("Rest curl yank") + assert.same( + "curl -sL 'https://api.github.com/users/boltlessengineer' '-X' 'GET' '-H' 'User-Agent: neovim'", + vim.fn.getreg("+") + ) + end) end) diff --git a/spec/cookie_jar_spec.lua b/spec/cookie_jar_spec.lua index d207f9d..ea1a5a8 100644 --- a/spec/cookie_jar_spec.lua +++ b/spec/cookie_jar_spec.lua @@ -7,78 +7,78 @@ local utils = require("rest-nvim.utils") local config = require("rest-nvim.config") describe("Cookies unit tests", function() - it("parse Set-Cookie header", function() - local url = "http://example.dev" - assert.is_same({ - name = "cookie1", - value = "value1", - domain = ".example.dev", - path = "/", - expires = -1, - }, jar.parse_set_cookie(url, "cookie1=value1")) - assert.is_same({ - name = "cookie2", - value = "", - domain = ".example.dev", - path = "/", - expires = -1, - }, jar.parse_set_cookie(url, "cookie2=")) - assert.is_same({ - name = "cookie3", - value = "value3", - domain = ".example.com", - path = "/", - expires = -1, - }, jar.parse_set_cookie(url, "cookie3=value3;domain=example.com")) - assert.is_same({ - name = "cookie4", - value = "value4", - domain = ".example.dev", - path = "/some/valid-path", - expires = -1, - }, jar.parse_set_cookie(url, "cookie4=value4;path=/some/valid-path")) - assert.is_same({ - name = "cookie5", - value = "value5", - domain = ".example.dev", - path = "/", - expires = 1723460761, - }, jar.parse_set_cookie(url, "cookie5=value5; Path=/; Expires=Mon, 12 Aug 2024 11:06:01 GMT")) - end) - it("update jar from response", function () - assert.is_same({}, jar.jar) - ---@diagnostic disable-next-line: missing-fields - jar.update_jar("http://example.dev", { - headers = { - ["set-cookie"] = { - "cookie1=value1", - "cookie2=value2;path=/some-path;cookie3=value3", - } - } - }) - assert.is_same({ - { - name = "cookie1", - value = "value1", - path = "/", - expires = -1, - domain = ".example.dev", - }, - { - name = "cookie2", - value = "value2", - path = "/some-path", - expires = -1, - domain = ".example.dev", - }, - }, jar.jar) - -- assert the cookies file - local lines = vim.split(utils.read_file(config.cookies.path), "\n") - assert.is_same({ - "# domain\tpath\tname\tvalue\texpires", - ".example.dev\t/\tcookie1\tvalue1\t-1", - ".example.dev\t/some-path\tcookie2\tvalue2\t-1", - "" - }, lines) - end) + it("parse Set-Cookie header", function() + local url = "http://example.dev" + assert.is_same({ + name = "cookie1", + value = "value1", + domain = ".example.dev", + path = "/", + expires = -1, + }, jar.parse_set_cookie(url, "cookie1=value1")) + assert.is_same({ + name = "cookie2", + value = "", + domain = ".example.dev", + path = "/", + expires = -1, + }, jar.parse_set_cookie(url, "cookie2=")) + assert.is_same({ + name = "cookie3", + value = "value3", + domain = ".example.com", + path = "/", + expires = -1, + }, jar.parse_set_cookie(url, "cookie3=value3;domain=example.com")) + assert.is_same({ + name = "cookie4", + value = "value4", + domain = ".example.dev", + path = "/some/valid-path", + expires = -1, + }, jar.parse_set_cookie(url, "cookie4=value4;path=/some/valid-path")) + assert.is_same({ + name = "cookie5", + value = "value5", + domain = ".example.dev", + path = "/", + expires = 1723460761, + }, jar.parse_set_cookie(url, "cookie5=value5; Path=/; Expires=Mon, 12 Aug 2024 11:06:01 GMT")) + end) + it("update jar from response", function() + assert.is_same({}, jar.jar) + ---@diagnostic disable-next-line: missing-fields + jar.update_jar("http://example.dev", { + headers = { + ["set-cookie"] = { + "cookie1=value1", + "cookie2=value2;path=/some-path;cookie3=value3", + }, + }, + }) + assert.is_same({ + { + name = "cookie1", + value = "value1", + path = "/", + expires = -1, + domain = ".example.dev", + }, + { + name = "cookie2", + value = "value2", + path = "/some-path", + expires = -1, + domain = ".example.dev", + }, + }, jar.jar) + -- assert the cookies file + local lines = vim.split(utils.read_file(config.cookies.path), "\n") + assert.is_same({ + "# domain\tpath\tname\tvalue\texpires", + ".example.dev\t/\tcookie1\tvalue1\t-1", + ".example.dev\t/some-path\tcookie2\tvalue2\t-1", + "", + }, lines) + end) end) diff --git a/spec/dotenv_spec.lua b/spec/dotenv_spec.lua index 47870d3..4ca18e8 100644 --- a/spec/dotenv_spec.lua +++ b/spec/dotenv_spec.lua @@ -3,29 +3,23 @@ require("spec.minimum_init") local function open(path) - vim.cmd.edit(path) - vim.cmd.source("ftplugin/http.lua") - return 0 + vim.cmd.edit(path) + vim.cmd.source("ftplugin/http.lua") + return 0 end local function remove_cwd(path) - path = path:gsub(vim.pesc(vim.fn.getcwd()) .. "/", "") - return path + path = path:gsub(vim.pesc(vim.fn.getcwd()) .. "/", "") + return path end -describe("dotenv", function () - it("find dotenv file with same name to http file", function () - open("spec/examples/dotenv/with_dotenv.http") - assert.same( - "spec/examples/dotenv/with_dotenv.env", - remove_cwd(vim.b._rest_nvim_env_file) - ) - end) - it("find dotenv file in parent dir", function () - open("spec/examples/dotenv/without_dotenv.http") - assert.same( - "spec/examples/.env", - remove_cwd(vim.b._rest_nvim_env_file) - ) - end) +describe("dotenv", function() + it("find dotenv file with same name to http file", function() + open("spec/examples/dotenv/with_dotenv.http") + assert.same("spec/examples/dotenv/with_dotenv.env", remove_cwd(vim.b._rest_nvim_env_file)) + end) + it("find dotenv file in parent dir", function() + open("spec/examples/dotenv/without_dotenv.http") + assert.same("spec/examples/.env", remove_cwd(vim.b._rest_nvim_env_file)) + end) end) diff --git a/spec/examples/examples_spec.lua b/spec/examples/examples_spec.lua index 3521ada..90eddf9 100644 --- a/spec/examples/examples_spec.lua +++ b/spec/examples/examples_spec.lua @@ -7,101 +7,101 @@ local utils = require("rest-nvim.utils") local Context = require("rest-nvim.context").Context local function open(path) - vim.cmd.edit(path) - vim.bo.filetype = "http" - vim.cmd.source("ftplugin/http.lua") - return 0 + vim.cmd.edit(path) + vim.bo.filetype = "http" + vim.cmd.source("ftplugin/http.lua") + return 0 end -describe("multi-line-url", function () - it("line breaks should be ignored", function () - local source = open("spec/examples/multi_line_url.http") - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - local req = parser.parse(req_node, source) - assert.not_nil(req) - ---@cast req rest.Request - assert.same("http://example.com:8080/api/html/get?id=123&value=content", req.url) - end) -end) - -describe("url without host", function () - local source = open("spec/examples/url_without_host.http") - local req_nodes = parser.get_all_request_nodes(source) - assert.same(4, #req_nodes) - it("host with non-secure port", function () - local req = parser.parse(req_nodes[1], source) - assert.not_nil(req) - ---@cast req rest.Request - assert.same("http://example.com:8080/api", req.url) - end) - it("host with secure port", function () - local req = parser.parse(req_nodes[2], source) - assert.not_nil(req) - ---@cast req rest.Request - assert.same("https://example.com:443/api", req.url) - end) - it("host with protocol", function () - local req = parser.parse(req_nodes[3], source) - assert.not_nil(req) - ---@cast req rest.Request - assert.same("http://example.com/api", req.url) - end) - it("host without protocol", function () - local req = parser.parse(req_nodes[4], source) - assert.not_nil(req) - ---@cast req rest.Request - assert.same("https://example.com/api", req.url) - end) +describe("multi-line-url", function() + it("line breaks should be ignored", function() + local source = open("spec/examples/multi_line_url.http") + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + local req = parser.parse(req_node, source) + assert.not_nil(req) + ---@cast req rest.Request + assert.same("http://example.com:8080/api/html/get?id=123&value=content", req.url) + end) end) -describe("in-place variables", function () - it("parse context sequentially", function () - local source = open("spec/examples/variables/in_place_variables.http") - local ctx = Context:new() - parser.eval_context(source, ctx, -1) - assert.same("", ctx:resolve("myhost")) - parser.eval_context(source, ctx, 0) - assert.same("", ctx:resolve("myhost")) - parser.eval_context(source, ctx, 1) - assert.same("example.org", ctx:resolve("myhost")) - parser.eval_context(source, ctx, 2) - assert.same("example.org", ctx:resolve("myhost")) - parser.eval_context(source, ctx, 12) - assert.same("example.net", ctx:resolve("myhost")) - end) - describe("evaluate context across multiple requests", function () - local source = open("spec/examples/variables/in_place_variables.http") +describe("url without host", function() + local source = open("spec/examples/url_without_host.http") local req_nodes = parser.get_all_request_nodes(source) - assert.same(3, #req_nodes) - local ctx = Context:new() - it("example 1", function () - local req1 = assert(parser.parse(req_nodes[1], source, ctx)) - ---@cast req1 rest.Request - assert.same("example.org/users", req1.url) + assert.same(4, #req_nodes) + it("host with non-secure port", function() + local req = parser.parse(req_nodes[1], source) + assert.not_nil(req) + ---@cast req rest.Request + assert.same("http://example.com:8080/api", req.url) + end) + it("host with secure port", function() + local req = parser.parse(req_nodes[2], source) + assert.not_nil(req) + ---@cast req rest.Request + assert.same("https://example.com:443/api", req.url) + end) + it("host with protocol", function() + local req = parser.parse(req_nodes[3], source) + assert.not_nil(req) + ---@cast req rest.Request + assert.same("http://example.com/api", req.url) end) - it("example 2", function () - local req2 = assert(parser.parse(req_nodes[2], source, ctx)) - assert.same("example.net/users", req2.url) + it("host without protocol", function() + local req = parser.parse(req_nodes[4], source) + assert.not_nil(req) + ---@cast req rest.Request + assert.same("https://example.com/api", req.url) end) - it("example 3", function () - local req3 = assert(parser.parse(req_nodes[3], source, ctx)) - assert.same("example.net/stats", req3.url) +end) + +describe("in-place variables", function() + it("parse context sequentially", function() + local source = open("spec/examples/variables/in_place_variables.http") + local ctx = Context:new() + parser.eval_context(source, ctx, -1) + assert.same("", ctx:resolve("myhost")) + parser.eval_context(source, ctx, 0) + assert.same("", ctx:resolve("myhost")) + parser.eval_context(source, ctx, 1) + assert.same("example.org", ctx:resolve("myhost")) + parser.eval_context(source, ctx, 2) + assert.same("example.org", ctx:resolve("myhost")) + parser.eval_context(source, ctx, 12) + assert.same("example.net", ctx:resolve("myhost")) + end) + describe("evaluate context across multiple requests", function() + local source = open("spec/examples/variables/in_place_variables.http") + local req_nodes = parser.get_all_request_nodes(source) + assert.same(3, #req_nodes) + local ctx = Context:new() + it("example 1", function() + local req1 = assert(parser.parse(req_nodes[1], source, ctx)) + ---@cast req1 rest.Request + assert.same("example.org/users", req1.url) + end) + it("example 2", function() + local req2 = assert(parser.parse(req_nodes[2], source, ctx)) + assert.same("example.net/users", req2.url) + end) + it("example 3", function() + local req3 = assert(parser.parse(req_nodes[3], source, ctx)) + assert.same("example.net/stats", req3.url) + end) end) - end) end) -describe("pre-request script", function () - local source = open("spec/examples/script/pre_request_script.http") - local req_nodes = parser.get_all_request_nodes(source) - assert.same(2, #req_nodes) - local ctx = Context:new() - it("set local variable from pre-request script", function () - local req1 = assert(parser.parse(req_nodes[1], source, ctx)) - assert.same("https://jsonplaceholder.typicode.com/posts/3", req1.url) - end) - it("local variables don't affect other requests", function () - local req2 = assert(parser.parse(req_nodes[2], source, ctx)) - assert.same("https://jsonplaceholder.typicode.com/posts/", req2.url) - end) +describe("pre-request script", function() + local source = open("spec/examples/script/pre_request_script.http") + local req_nodes = parser.get_all_request_nodes(source) + assert.same(2, #req_nodes) + local ctx = Context:new() + it("set local variable from pre-request script", function() + local req1 = assert(parser.parse(req_nodes[1], source, ctx)) + assert.same("https://jsonplaceholder.typicode.com/posts/3", req1.url) + end) + it("local variables don't affect other requests", function() + local req2 = assert(parser.parse(req_nodes[2], source, ctx)) + assert.same("https://jsonplaceholder.typicode.com/posts/", req2.url) + end) end) diff --git a/spec/minimum_init.lua b/spec/minimum_init.lua index 4843623..d6a02a2 100644 --- a/spec/minimum_init.lua +++ b/spec/minimum_init.lua @@ -4,23 +4,23 @@ local rest_nvim_dir = vim.fn.fnamemodify(test_dir, ":h") vim.opt.rtp:prepend(vim.fs.normalize("/home/ubuntu/projects/tree-sitter-http")) local parser_dir = "$HOME/.cache/tree-sitter/lib/http.so" if vim.fn.has("macunix") == 1 then - parser_dir = "$HOME/Library/Caches/tree-sitter/lib/http.dylib" + parser_dir = "$HOME/Library/Caches/tree-sitter/lib/http.dylib" end vim.treesitter.language.add("http", { path = vim.fs.normalize(parser_dir) }) if not vim.treesitter.language.get_lang("http") then - vim.treesitter.language.register("http", "http") + vim.treesitter.language.register("http", "http") end vim.opt.runtimepath:append(rest_nvim_dir) vim.g.rest_nvim = { - _log_level = vim.log.levels.INFO, - request = { - hooks = { - user_agent = "", - } - }, - cookies = { - path = "/tmp/rest-nvim.cookies" - }, + _log_level = vim.log.levels.INFO, + request = { + hooks = { + user_agent = "", + }, + }, + cookies = { + path = "/tmp/rest-nvim.cookies", + }, } ---@diagnostic disable-next-line: undefined-field vim.uv.fs_unlink(vim.g.rest_nvim.cookies.path) diff --git a/spec/parser/http_parser_spec.lua b/spec/parser/http_parser_spec.lua index fc341b1..756a694 100644 --- a/spec/parser/http_parser_spec.lua +++ b/spec/parser/http_parser_spec.lua @@ -10,47 +10,47 @@ local logger = require("rest-nvim.logger") local spy = require("luassert.spy") local function open(path) - vim.cmd.edit(path) - vim.cmd.source("ftplugin/http.lua") - return 0 + vim.cmd.edit(path) + vim.cmd.source("ftplugin/http.lua") + return 0 end ---@diagnostic disable-next-line: duplicate-set-field vim.notify = function() end describe("http parser", function() - it("validate http parser", function() - assert.same("http", vim.treesitter.language.get_lang("http")) - end) - it("parse from http string", function() - local source = "GET https://github.com\n" - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - assert.same({ - method = "GET", - url = "https://github.com", - headers = {}, - cookies = {}, - handlers = {}, - }, parser.parse(req_node, source)) - end) - it("parse from http file", function() - local source = open("spec/examples/basic_get.http") - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - assert.same({ - name = "basic_get#1", - method = "GET", - url = "https://api.github.com/users/boltlessengineer", - headers = { - ["user-agent"] = { "neovim" }, - }, - cookies = {}, - handlers = {}, - }, parser.parse(req_node, source)) - end) - it("capture all request names", function () - local source = [[ + it("validate http parser", function() + assert.same("http", vim.treesitter.language.get_lang("http")) + end) + it("parse from http string", function() + local source = "GET https://github.com\n" + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + method = "GET", + url = "https://github.com", + headers = {}, + cookies = {}, + handlers = {}, + }, parser.parse(req_node, source)) + end) + it("parse from http file", function() + local source = open("spec/examples/basic_get.http") + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + name = "basic_get#1", + method = "GET", + url = "https://api.github.com/users/boltlessengineer", + headers = { + ["user-agent"] = { "neovim" }, + }, + cookies = {}, + handlers = {}, + }, parser.parse(req_node, source)) + end) + it("capture all request names", function() + local source = [[ ### first named request GET http://localhost:80 ### request separator that isn't a request name @@ -59,68 +59,68 @@ GET http://localhost:80 # additional comments GET http://localhost:80 ]] - local names = parser.get_request_names(source) - assert.same({"first named request", "second named request"}, names) - end) - it("parse request with host header", function () - local source = [[ + local names = parser.get_request_names(source) + assert.same({ "first named request", "second named request" }, names) + end) + it("parse request with host header", function() + local source = [[ GET /some/path HOST: localhost:8000 ]] - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - local req = assert(parser.parse(req_node, source)) - assert.same("http://localhost:8000/some/path", req.url) - end) - it("parse request with headers", function () - local source = [[ + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + local req = assert(parser.parse(req_node, source)) + assert.same("http://localhost:8000/some/path", req.url) + end) + it("parse request with headers", function() + local source = [[ http://example.com/api X-Header1: value1 X-Header2: X-Header1: value2 ]] - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - assert.same({ - url = "http://example.com/api", - method = "GET", - headers = { - ["x-header1"] = { "value1", "value2" }, - ["x-header2"] = {}, - }, - handlers = {}, - cookies = {}, - }, parser.parse(req_node, source)) - end) - - describe("parse body", function() - it("json body", function() - local source = 'POST https://example.com\n\n{\n\t"blah": 1}\n' - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - assert.same({ - method = "POST", - url = "https://example.com", - headers = {}, - cookies = {}, - handlers = {}, - body = { - __TYPE = "json", - data = '{\n\t"blah": 1}', - }, - }, parser.parse(req_node, source)) + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + url = "http://example.com/api", + method = "GET", + headers = { + ["x-header1"] = { "value1", "value2" }, + ["x-header2"] = {}, + }, + handlers = {}, + cookies = {}, + }, parser.parse(req_node, source)) end) - it("invalid json body", function() - local source = 'POST https://example.com\n\n{\n\t"blah": 1\n' - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - local spy_log_warn = spy.on(logger, "warn") - parser.parse(req_node, source) - ---@diagnostic disable-next-line: undefined-field - assert.spy(spy_log_warn).called_with("invalid json: '{\n\t\"blah\": 1'") - end) - it("parse xml", function() - local source = [[POST https://example.com + + describe("parse body", function() + it("json body", function() + local source = 'POST https://example.com\n\n{\n\t"blah": 1}\n' + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + method = "POST", + url = "https://example.com", + headers = {}, + cookies = {}, + handlers = {}, + body = { + __TYPE = "json", + data = '{\n\t"blah": 1}', + }, + }, parser.parse(req_node, source)) + end) + it("invalid json body", function() + local source = 'POST https://example.com\n\n{\n\t"blah": 1\n' + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + local spy_log_warn = spy.on(logger, "warn") + parser.parse(req_node, source) + ---@diagnostic disable-next-line: undefined-field + assert.spy(spy_log_warn).called_with("invalid json: '{\n\t\"blah\": 1'") + end) + it("parse xml", function() + local source = [[POST https://example.com @@ -128,36 +128,36 @@ X-Header1: value2 password ]] - local _, tree = utils.ts_parse_source(source) - local req_node = assert(tree:root():child(0)) - assert.same({ - method = "POST", - url = "https://example.com", - headers = {}, - cookies = {}, - handlers = {}, - body = { - __TYPE = "xml", - data = [[ + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + method = "POST", + url = "https://example.com", + headers = {}, + cookies = {}, + handlers = {}, + body = { + __TYPE = "xml", + data = [[ login password ]], - }, - }, parser.parse(req_node, source)) - end) - it("parse invalid xml", function() - logger.info("hi") - local source = "POST https://example.com\n\n