From b388cee1ad57e59a8d56d9e90b0fea1c47288a03 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 14 Feb 2025 05:57:28 +0000 Subject: [PATCH 1/3] feat: interpret basic authorization token Add request hook to convert "Authorization: Basic username:passeord" to "Authorization: Basic " before reqeust. --- README.md | 3 +++ doc/rest-nvim.txt | 12 ++++++---- lua/rest-nvim/autocmds.lua | 8 +++++++ lua/rest-nvim/config/default.lua | 3 +++ lua/rest-nvim/config/init.lua | 5 +++++ nix/plugin-overlay.nix | 20 +++++++++++++++++ nix/test-overlay.nix | 1 + rest.nvim-scm-1.rockspec | 1 + spec/examples/examples_spec.lua | 38 ++++++++++++++++++++++++++++++++ 9 files changed, 87 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f1ccca2b..77fb9181 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ local default_config = { user_agent = "rest.nvim v" .. require("rest-nvim.api").VERSION, ---@type boolean Set `Content-Type` header when it is empty and body is provided set_content_type = true, + ---@type boolean Interpret `Authorization` header when it is set in form of + ---"Basic username:password" or "Basic username password" + interpret_basic_auth = true, }, }, ---@class rest.Config.Response diff --git a/doc/rest-nvim.txt b/doc/rest-nvim.txt index bfb9acee..353eb9b5 100644 --- a/doc/rest-nvim.txt +++ b/doc/rest-nvim.txt @@ -149,10 +149,14 @@ rest.Opts.Request *rest.Opts.Request* rest.Opts.Request.Hooks *rest.Opts.Request.Hooks* Fields: ~ - {encode_url?} (boolean) Encode URL before making request (Default: `true`) - {user_agent?} (string) Set `User-Agent` header when it is empty. Set as empty string to disable. - (Default: `rest.nvim {version}`) - {set_content_type?} (boolean) Set `Content-Type` header when it is empty but request body is provided + {encode_url?} (boolean) Encode URL before making request (Default: `true`) + {user_agent?} (string) Set `User-Agent` header when it is empty. Set as empty string to disable. + (Default: `rest.nvim {version}`) + {set_content_type?} (boolean) Set `Content-Type` header when it is empty but request body is provided + {interpret_basic_auth?} (boolean) Interpret `Authorization` header when it is set in form of + "Basic username:password" or "Basic username password" + It will convert header to "Basic " + (Default: `true`) rest.Opts.Response *rest.Opts.Response* diff --git a/lua/rest-nvim/autocmds.lua b/lua/rest-nvim/autocmds.lua index 8def9144..7e939c01 100644 --- a/lua/rest-nvim/autocmds.lua +++ b/lua/rest-nvim/autocmds.lua @@ -81,6 +81,14 @@ function autocmds.setup() end end end + if hooks.interpret_basic_auth then + local auth_header = req.headers["authorization"] + if auth_header then + local auth_header_value = auth_header[#auth_header] + local id, pw = auth_header_value:match("Basic%s+([^:%s]+)%s*[:(%s+)]%s*(.*)") + auth_header[#auth_header] = "Basic " .. require("base64").encode(id .. ":" .. pw) + end + end end, }) vim.api.nvim_create_autocmd("User", { diff --git a/lua/rest-nvim/config/default.lua b/lua/rest-nvim/config/default.lua index 565a8014..5d700e2b 100644 --- a/lua/rest-nvim/config/default.lua +++ b/lua/rest-nvim/config/default.lua @@ -19,6 +19,9 @@ local default_config = { user_agent = "rest.nvim v" .. require("rest-nvim.api").VERSION, ---@type boolean Set `Content-Type` header when it is empty and body is provided set_content_type = true, + ---@type boolean Interpret `Authorization` header when it is set in form of + ---"Basic username:password" or "Basic username password" + interpret_basic_auth = true, }, }, ---@class rest.Config.Response diff --git a/lua/rest-nvim/config/init.lua b/lua/rest-nvim/config/init.lua index 5b9a3eb1..0bb63c13 100644 --- a/lua/rest-nvim/config/init.lua +++ b/lua/rest-nvim/config/init.lua @@ -43,6 +43,11 @@ local config ---@field user_agent? string --- Set `Content-Type` header when it is empty but request body is provided ---@field set_content_type? boolean +--- Interpret `Authorization` header when it is set in form of +--- "Basic username:password" or "Basic username password" +--- It will convert header to "Basic " +--- (Default: `true`) +---@field interpret_basic_auth? boolean ---@class rest.Opts.Response --- Default response hooks (aka. request handlers) configuration diff --git a/nix/plugin-overlay.nix b/nix/plugin-overlay.nix index 7a44abb1..f60f8d1b 100644 --- a/nix/plugin-overlay.nix +++ b/nix/plugin-overlay.nix @@ -1,6 +1,25 @@ { self }: final: prev: let luaPackages-override = luaself: luaprev: { tree-sitter-http = luaself.callPackage ./tree-sitter-http.nix {}; + base64 = luaself.callPackage ({ + buildLuarocksPackage, + fetchurl, + fetchzip, + luaOlder, + }: + buildLuarocksPackage { + pname = "base64"; + version = "1.5-3"; + knownRockspec = (fetchurl { + url = "mirror://luarocks/base64-1.5-3.rockspec"; + sha256 = "sha256-jMWugDDJMOfapPyEWVrC2frF24MSr8pLsNtwK9ejLwM="; + }).outPath; + src = fetchzip { + url = "https://github.com/iskolbin/lbase64/archive/c261320edbdf82c16409d893a96c28c704aa0ab8.zip"; + sha256 = "sha256-Ucu7pxEbyeUyV12+FeFbGNhXiKsGYlXFMrb1Vhsnfrc="; + }; + disabled = luaOlder "5.1"; + }) {}; rest-nvim = luaself.callPackage ({ buildLuarocksPackage, fetchurl, @@ -19,6 +38,7 @@ mimetypes xml2lua fidget-nvim + base64 tree-sitter-http ]; }) {}; diff --git a/nix/test-overlay.nix b/nix/test-overlay.nix index 76ac1484..621fa74e 100644 --- a/nix/test-overlay.nix +++ b/nix/test-overlay.nix @@ -14,6 +14,7 @@ mimetypes xml2lua fidget-nvim + final.lua51Packages.base64 final.lua51Packages.tree-sitter-http ]; extraPackages = [ diff --git a/rest.nvim-scm-1.rockspec b/rest.nvim-scm-1.rockspec index ef29fc28..28990313 100644 --- a/rest.nvim-scm-1.rockspec +++ b/rest.nvim-scm-1.rockspec @@ -25,6 +25,7 @@ dependencies = { "mimetypes", "xml2lua", "fidget.nvim", + "base64", "tree-sitter-http == 0.0.35", } diff --git a/spec/examples/examples_spec.lua b/spec/examples/examples_spec.lua index 67e883c0..8d72fbe3 100644 --- a/spec/examples/examples_spec.lua +++ b/spec/examples/examples_spec.lua @@ -119,4 +119,42 @@ describe("builtin request hooks", function() assert.same({ "application/json" }, req.headers["content-type"]) end) end) + ---@return rest.Request + local function sample_request(opts) + return vim.tbl_deep_extend("keep", opts, { + method = "GET", + url = "https://example.com", + headers = {}, + cookies = {}, + handlers = {}, + }) + end + describe("interpret_basic_auth", function() + it("with valid vscode style token", function() + local req = sample_request({ + headers = { + ["authorization"] = { "Basic username:password" }, + }, + }) + _G.rest_request = req + vim.api.nvim_exec_autocmds("User", { + pattern = { "RestRequest", "RestRequestPre" }, + }) + _G.rest_request = nil + assert.same({ "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" }, req.headers["authorization"]) + end) + it("with valid intellij style token", function() + local req = sample_request({ + headers = { + ["authorization"] = { "Basic username password" }, + }, + }) + _G.rest_request = req + vim.api.nvim_exec_autocmds("User", { + pattern = { "RestRequest", "RestRequestPre" }, + }) + _G.rest_request = nil + assert.same({ "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" }, req.headers["authorization"]) + end) + end) end) From 40ceee1cbacf90c35add09e9005173a6ae3ebd20 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 14 Feb 2025 08:50:45 +0000 Subject: [PATCH 2/3] wip: add md5 dependency --- lua/rest-nvim/autocmds.lua | 28 ++++++++++++++++++++++------ nix/plugin-overlay.nix | 20 ++++++++++++++++++++ nix/test-overlay.nix | 1 + spec/examples/examples_spec.lua | 6 ++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lua/rest-nvim/autocmds.lua b/lua/rest-nvim/autocmds.lua index 7e939c01..d421e018 100644 --- a/lua/rest-nvim/autocmds.lua +++ b/lua/rest-nvim/autocmds.lua @@ -43,6 +43,27 @@ local autocmds = {} +---@param req rest.Request +local function interpret_basic_auth(req) + local auth_header = req.headers["authorization"] + if not auth_header then + return + end + local auth_header_value = auth_header[#auth_header] + local auth_type, id, pw = auth_header_value:match("(%w+)%s+([^:%s]+)%s*[:(%s+)]%s*(.*)") + if auth_type == "Basic" then + auth_header[#auth_header] = "Basic " .. require("base64").encode(id .. ":" .. pw) + elseif auth_type == "Digest" then + -- TODO: implement digest tokens... but how? + -- we can update headers but digest tokens require multiple requests + -- So it can only be supported after we have chained requests support. + -- see https://github.com/catwell/lua-http-digest/blob/master/http-digest.lua + -- as example implementation of digest tokens + else + require("rest-nvim.logger").info("Unsupported auth-type:", auth_type) + end +end + ---Set up Rest autocommands group ---@package function autocmds.setup() @@ -82,12 +103,7 @@ function autocmds.setup() end end if hooks.interpret_basic_auth then - local auth_header = req.headers["authorization"] - if auth_header then - local auth_header_value = auth_header[#auth_header] - local id, pw = auth_header_value:match("Basic%s+([^:%s]+)%s*[:(%s+)]%s*(.*)") - auth_header[#auth_header] = "Basic " .. require("base64").encode(id .. ":" .. pw) - end + interpret_basic_auth(req) end end, }) diff --git a/nix/plugin-overlay.nix b/nix/plugin-overlay.nix index f60f8d1b..e9bd30fd 100644 --- a/nix/plugin-overlay.nix +++ b/nix/plugin-overlay.nix @@ -20,6 +20,25 @@ }; disabled = luaOlder "5.1"; }) {}; + md5 = luaself.callPackage ({ + buildLuarocksPackage, + fetchurl, + fetchzip, + luaOlder, + }: + buildLuarocksPackage { + pname = "md5"; + version = "1.3-1"; + knownRockspec = (fetchurl { + url = "mirror://luarocks/md5-1.3-1.rockspec"; + sha256 = "sha256-2S6Dvk0yazW8DCJf2DYxrLW11+B7r6DCJZ7CMCMAfSI="; + }).outPath; + src = fetchzip { + url = "https://github.com/lunarmodules/md5/archive/refs/tags/1.3.zip"; + sha256 = "sha256-mCN7mmCGf2vdXriT1q8rrhUmKh49AY8A+cZVCu6XJzY="; + }; + disabled = luaOlder "5.0"; + }) {}; rest-nvim = luaself.callPackage ({ buildLuarocksPackage, fetchurl, @@ -39,6 +58,7 @@ xml2lua fidget-nvim base64 + md5 tree-sitter-http ]; }) {}; diff --git a/nix/test-overlay.nix b/nix/test-overlay.nix index 621fa74e..6dfd9b00 100644 --- a/nix/test-overlay.nix +++ b/nix/test-overlay.nix @@ -15,6 +15,7 @@ xml2lua fidget-nvim final.lua51Packages.base64 + final.lua51Packages.md5 final.lua51Packages.tree-sitter-http ]; extraPackages = [ diff --git a/spec/examples/examples_spec.lua b/spec/examples/examples_spec.lua index 8d72fbe3..50f24360 100644 --- a/spec/examples/examples_spec.lua +++ b/spec/examples/examples_spec.lua @@ -158,3 +158,9 @@ describe("builtin request hooks", function() end) end) end) +it("make sure md5 work", function() + local md5 = require("md5") + local md5sum = md5.sumhexa + assert.same("9236657b478ea807fdfa275d24990843", md5sum("qwer:asdf")) + -- TODO: implement digest auth with https://github.com/catwell/lua-http-digest/blob/master/http-digest.lua +end) From fc8ad470683461e96e11d4b8dbec55d4d98ca547 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 5 Mar 2025 14:58:28 +0000 Subject: [PATCH 3/3] wip: refactor? --- lua/rest-nvim/parser/init_.lua | 553 +++++++++++++++++++++++++++++++++ lua/rest-nvim/request.lua | 3 + 2 files changed, 556 insertions(+) create mode 100644 lua/rest-nvim/parser/init_.lua diff --git a/lua/rest-nvim/parser/init_.lua b/lua/rest-nvim/parser/init_.lua new file mode 100644 index 00000000..93ae0b69 --- /dev/null +++ b/lua/rest-nvim/parser/init_.lua @@ -0,0 +1,553 @@ +---@mod rest-nvim.parser rest.nvim http syntax parsing module +--- +---@brief [[ +--- +--- rest.nvim `.http` syntax parsing module. +--- rest.nvim uses `tree-sitter-http` as a core parser for `.http` syntax +--- +---@brief ]] + +local parser = {} + +local Context = require("rest-nvim.context").Context +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", + [[ +(section + (request_separator + value: (_) @name) + request: (_)) @request +(section + (comment + name: (_) @_keyword + 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 +end + +---@param src string +---@param context rest.Context +---@return string +---@return integer +local function expand_variables(src, context) + return src:gsub("{{(.-)}}", function(name) + name = vim.trim(name) + local res = context:resolve(name) + return res + end) +end + +---@param req_node TSNode Tree-sitter request node +---@param source Source +---@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] = {} + end + end + return setmetatable(headers, nil) +end + +---@param str string +---@return boolean +local function validate_json(str) + 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 +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("&") +end + +---@param content_type string? +---@param body_node TSNode +---@param source Source +---@param context rest.Context +---@return rest.Request.Body|nil +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, + } + local body_text = vim.treesitter.get_node_text(body_node, source) + if vim.startswith(body_text, "<@") then + logger.debug("external body with '<@' prefix") + return body + end + local file_content = utils.read_file(path) + file_content = expand_variables(file_content, context) + body.data.content = file_content + elseif node_type == "graphql_body" then + body.__TYPE = "graphql" + local query_text = vim.treesitter.get_node_text(assert(body_node:named_child(0)), source) + query_text = expand_variables(query_text, context) + local variables_text + local variables_node = body_node:named_child(1) + if variables_node then + variables_text = vim.treesitter.get_node_text(variables_node, source) + variables_text = expand_variables(variables_text, context) + end + body.data = vim.json.encode({ + query = query_text, + variables = variables_text and vim.json.decode(variables_text), + }) + logger.debug(body.data) + 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) + body.data = expand_variables(body.data, context) + 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") + end + return body +end + +---In-place variables can be evaluated in loaded buffers due to treesitter limitations +---@param source integer +---@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 + end + end +end + +---@return TSNode? node TSNode with type `section` +function parser.get_request_node_by_cursor() + 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 + 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) + end + end + 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 + 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) +end + +---@param node TSNode +---@param source Source +---@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 + 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 +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)() +end + +---@param node TSNode +---@param source Source +---@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) +end + +---@param node TSNode +---@param source Source +---@param ctx rest.Context +---@return function? +function parser.parse_redirect_path(node, source, ctx) + local force = vim.treesitter.get_node_text(node, source):match("^>>!") + local path = get_node_field_text(node, "path", source) + if path then + path = expand_variables(path, ctx) + return function(res) + if not res.body then + return + end + logger.debug("save response body to:", path) + if not force then + local suffix_idx = 1 + while utils.file_exists(path) do + local pathname, pathext = path:match("([^.]+)(.*)") + path = ("%s_%d%s"):format(pathname, suffix_idx, pathext) + suffix_idx = suffix_idx + 1 + end + end + local respfile, openerr = io.open(path, "w+") + if not respfile then + local err_msg = string.format("Failed to open response file (%s): %s", path, openerr) + vim.notify(err_msg, vim.log.levels.ERROR, { title = "rest.nvim" }) + return + end + respfile:write(res.body) + respfile:close() + logger.debug("response body saved done") + end + end +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)) + end + end + return result +end + +---@param name string|nil +---@return TSNode? +function parser.get_request_node(name) + local req_node + if not name then + req_node = parser.get_request_node_by_cursor() + 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 + else + 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 + end + return req_node +end + +---Parse the request node and create Request object. Returns `nil` if parsing +---failed. +---@param node TSNode Tree-sitter request node +---@param source Source +---@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) + + ctx = ctx or Context:new() + -- TODO: note that in-place variables won't be evaluated for raw string 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 + if method == "GRAPHQL" then + method = "POST" + end + -- NOTE: url will be parsed after because in-place variables should be parsed first + local url + + ---@type string|nil + 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) + url = url:gsub("\n%s+", "") + elseif child_type == "pre_request_script" then + parser.parse_pre_request_script(child, source, ctx) + -- won't be a case anymore with latest tree-sitter-http parser. just for backward compatibility + 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 child:field("name")[1] then + local comment_name = get_node_field_text(child, "name", source) + local comment_value = get_node_field_text(child, "value", source) + if comment_name == "name" then + name = comment_value or name + elseif comment_name == "prompt" and comment_value then + local var_name, var_description = comment_value:match("(%S+)%s*(.*)") + if var_description == "" then + var_description = nil + end + vim.ui.input({ + prompt = (var_description or ("Enter value for `%s`"):format(var_name)) .. ": ", + default = ctx:resolve(var_name), + }, function(input) + if input then + ctx:set_local(var_name, input) + end + end) + end + elseif child_type == "variable_declaration" then + parser.parse_variable_declaration(child, source, ctx) + end + end + for child, _ in req_node:iter_children() do + local child_type = child:type() + if child_type == "res_handler_script" then + logger.debug("find request node child:", child_type) + local handler = parser.parse_request_handler(child, source, ctx) + if handler then + table.insert(handlers, handler) + end + elseif child_type == "res_redirect" then + logger.debug("find request node child:", child_type) + local handler = parser.parse_redirect_path(child, source, ctx) + if handler then + table.insert(handlers, handler) + 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 + 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 + 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 + 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 +end + +return parser diff --git a/lua/rest-nvim/request.lua b/lua/rest-nvim/request.lua index 01b2c6fc..fc261554 100644 --- a/lua/rest-nvim/request.lua +++ b/lua/rest-nvim/request.lua @@ -27,6 +27,9 @@ local Context = require("rest-nvim.context").Context ---@field body? rest.Request.Body ---@field handlers fun()[] +---@class rest.RequestSpec: rest.Request +---@field next_request? rest.Request + ---@class rest.Response ---@field status rest.Response.status Status information from response ---@field body string? Raw response body