diff --git a/.editorconfig b/.editorconfig index 482d32a..2d26f47 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,9 @@ trim_trailing_whitespace = true [*.lua] indent_size = 2 +[*.scm] +indent_size = 2 + [*.{md,lua}] max_line_length = 100 diff --git a/lua/rest-nvim/parser/init.lua b/lua/rest-nvim/parser/init.lua index 3645a77..8af7911 100644 --- a/lua/rest-nvim/parser/init.lua +++ b/lua/rest-nvim/parser/init.lua @@ -72,52 +72,99 @@ local function parse_headers(req_node, source, context) 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 table? +local function parse_urlencoded_form(str) + local form = {} + local query_pairs = vim.split(str, "&") + for _, query in ipairs(query_pairs) do + local key, value = query:match("([^=]+)=?(.*)") + if not key then + -- TODO: error + return nil + end + form[vim.trim(key)] = vim.trim(value) + end + return form +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(body_node, source, context) +---@return rest.Request.Body? +function parser.parse_body(content_type, body_node, source, context) local body = {} - body.__TYPE = body_node:type():gsub("_%w+", "") + local node_type = body_node:type() ---@cast body rest.Request.Body - if body.__TYPE == "json" then + 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.api.nvim_buf_get_name(source):match("(.*)/.*") + 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, _ = pcall(vim.json.decode, body.data) + local ok = validate_json(body.data) if not ok then logger.warn("invalid json: '" .. body.data .. "'") return nil end - elseif body.__TYPE == "xml" then + 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 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, body.data) + local ok = validate_xml(body.data) if not ok then logger.warn("invalid xml: '" .. body.data .. "'") return nil end - elseif body.__TYPE == "form" then - body.data = {} - for pair, _ in body_node:iter_children() do - if pair:type() == "query" then - local key = assert(get_node_field_text(pair, "key", source)) - local value = assert(get_node_field_text(pair, "value", source)) - key = expand_variables(key, context) - value = expand_variables(value, context) - body.data[key] = value + elseif node_type == "raw_body" then + -- TODO: exclude comments from text + 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 = "form" + body.data = parse_urlencoded_form(text) + if not body.data then + -- TODO: parsing urlencoded form failed + return nil end + else + body.__TYPE = "raw" + body.data = text end - elseif body.__TYPE == "external" then - local path = assert(get_node_field_text(body_node, "path", source)) - path = vim.fs.normalize(vim.fs.joinpath(vim.fn.expand("%:h"), path)) - body.data = { - name = get_node_field_text(body_node, "name", source), - path = path, - } - elseif body.__TYPE == "graphql" then + 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 @@ -273,15 +320,6 @@ function parser.parse(node, source, ctx) logger.error("request section doesn't have request node") return nil end - local body - local body_node = req_node:field("body")[1] - if body_node then - body = parser.parse_body(body_node, source, ctx) - if not body then - logger.error("parsing body failed") - return nil - end - end local method = get_node_field_text(req_node, "method", source) if not method then logger.info("no method provided, falling back to 'GET'") @@ -334,6 +372,23 @@ function parser.parse(node, source, ctx) url = host..url table.remove(headers["host"], 1) end + + ---@type string? + local content_type + if headers["content-type"] 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("[rest.nvim] parsing request body failed. See `:Rest logs` for more info.", vim.log.levels.ERROR) + return nil + end + end + ---@type rest.Request local req = { name = name, diff --git a/lua/rest-nvim/request.lua b/lua/rest-nvim/request.lua index 45e1758..cca0d23 100644 --- a/lua/rest-nvim/request.lua +++ b/lua/rest-nvim/request.lua @@ -12,7 +12,7 @@ local jar = require("rest-nvim.cookie_jar") local clients = require("rest-nvim.client") ---@class rest.Request.Body ----@field __TYPE "json"|"xml"|"external"|"form"|"graphql" +---@field __TYPE "json"|"xml"|"raw"|"graphql"|"multipart_form_data"|"form"|"external" ---@field data any ---@class rest.Request diff --git a/lua/rest-nvim/ui/result.lua b/lua/rest-nvim/ui/result.lua index 08fdc5d..e5a4197 100644 --- a/lua/rest-nvim/ui/result.lua +++ b/lua/rest-nvim/ui/result.lua @@ -70,7 +70,7 @@ local panes = { 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") + table.insert(lines, "# @_RES") local body = vim.split(data.response.body, "\n") if content_type then local res_type = content_type[1]:match(".*/([^;]+)") @@ -81,7 +81,7 @@ local panes = { end end vim.list_extend(lines, body) - table.insert(lines, "#+END") + table.insert(lines, "# @_END") else vim.list_extend(lines, { "", "# Loading..." }) end diff --git a/queries/http/highlights.scm b/queries/http/highlights.scm index 700683b..2e0fcb0 100644 --- a/queries/http/highlights.scm +++ b/queries/http/highlights.scm @@ -9,17 +9,16 @@ (variable_declaration name: (identifier) @variable) -; Parameters -(query_param - key: (_) @variable.parameter) - ; Operators -[ - "=" - "&" - "@" - "<" -] @operator +(comment + "=" @operator) +(variable_declaration + "=" @operator) + +; keywords +(comment + "@" @keyword + name: (_) @keyword) ; Literals (request @@ -37,7 +36,8 @@ "}}" ] @punctuation.bracket -":" @punctuation.delimiter +(header + ":" @punctuation.delimiter) ; external JSON body (external_body diff --git a/queries/http/injections.scm b/queries/http/injections.scm index ad54353..4ce8075 100644 --- a/queries/http/injections.scm +++ b/queries/http/injections.scm @@ -9,10 +9,20 @@ ((xml_body) @injection.content (#set! injection.language "xml")) -((graphql_body) @injection.content +((graphql_data) @injection.content (#set! injection.language "graphql")) -; Lua scripting +; Script (default to javascript) ((script) @injection.content (#offset! @injection.content 0 2 0 -2) - (#set! injection.language "lua")) + (#set! injection.language "javascript")) + +; Script with other languages +((comment + name: (_) @_name + (#eq? @_name "lang") + value: (_) @injection.language) + . + (_ + (script) @injection.content + (#offset! @injection.content 0 2 0 -2))) diff --git a/spec/examples/post_with_external_body.http b/spec/examples/post_with_external_body.http new file mode 100644 index 0000000..53b9b95 --- /dev/null +++ b/spec/examples/post_with_external_body.http @@ -0,0 +1,5 @@ +// The request body is read from a file +POST https://example.com:8080/api/html/post +Content-Type: application/json + +< ./input.json diff --git a/spec/parser/http_parser_spec.lua b/spec/parser/http_parser_spec.lua new file mode 100644 index 0000000..aad17a4 --- /dev/null +++ b/spec/parser/http_parser_spec.lua @@ -0,0 +1,69 @@ +---@module 'luassert' + +require("spec.minimum_init") + +local parser = require("rest-nvim.parser") +local utils = require("rest-nvim.utils") + +local function open(path) + vim.cmd.edit(path) + vim.cmd.source("ftplugin/http.lua") + return 0 +end + +describe("parser", function () + it("parse form-urlencoded body", function () + local source = [[ +POST https://ijhttp-examples.jetbrains.com/post +Content-Type: application/x-www-form-urlencoded + +key1 = value1 & +key2 = value2 & +key3 = value3 & +key4 = value4 & +key5 = value5 +]] + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + method = "POST", + url = "https://ijhttp-examples.jetbrains.com/post", + headers = { + ["content-type"] = { "application/x-www-form-urlencoded" }, + }, + cookies = {}, + handlers = {}, + body = { + __TYPE = "form", + data = { + key1 = "value1", + key2 = "value2", + key3 = "value3", + key4 = "value4", + key5 = "value5", + }, + }, + }, parser.parse(req_node, source)) + end) + it("parse external body", function () + -- external body can be only sourced when + local source = open("spec/examples/post_with_external_body.http") + local _, tree = utils.ts_parse_source(source) + local req_node = assert(tree:root():child(0)) + assert.same({ + method = "POST", + url = "https://example.com:8080/api/html/post", + headers = { + ["content-type"] = { "application/json" }, + }, + cookies = {}, + handlers = {}, + body = { + __TYPE = "external", + data = { + path = "spec/examples/input.json" + } + }, + }, parser.parse(req_node, source)) + end) +end) diff --git a/tree-sitter-http b/tree-sitter-http index 9e91cf9..448f761 160000 --- a/tree-sitter-http +++ b/tree-sitter-http @@ -1 +1 @@ -Subproject commit 9e91cf9c79f4ed22525209f98e92021b404560fd +Subproject commit 448f76120633e5c9d1c0185c93b3e68d7ff4e03b