Skip to content

Commit 6ab4cb0

Browse files
feat: bump version of tree-sitter-http parser
- raw body support - queries - more highlights - script language is javascript by default - more tests
1 parent 377ad65 commit 6ab4cb0

File tree

9 files changed

+196
-54
lines changed

9 files changed

+196
-54
lines changed

.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ trim_trailing_whitespace = true
1111
[*.lua]
1212
indent_size = 2
1313

14+
[*.scm]
15+
indent_size = 2
16+
1417
[*.{md,lua}]
1518
max_line_length = 100
1619

lua/rest-nvim/parser/init.lua

+91-36
Original file line numberDiff line numberDiff line change
@@ -72,52 +72,99 @@ local function parse_headers(req_node, source, context)
7272
return setmetatable(headers, nil)
7373
end
7474

75+
---@param str string
76+
---@return boolean
77+
local function validate_json(str)
78+
local ok, _ = pcall(vim.json.decode, str)
79+
return ok
80+
end
81+
82+
---@param str string
83+
---@return boolean
84+
local function validate_xml(str)
85+
local xml2lua = require("xml2lua")
86+
local handler = require("xmlhandler.tree"):new()
87+
local xml_parser = xml2lua.parser(handler)
88+
local ok = pcall(function (t) return xml_parser:parse(t) end, str)
89+
return ok
90+
end
91+
92+
---@param str string
93+
---@return table<string,string>?
94+
local function parse_urlencoded_form(str)
95+
local form = {}
96+
local query_pairs = vim.split(str, "&")
97+
for _, query in ipairs(query_pairs) do
98+
local key, value = query:match("([^=]+)=?(.*)")
99+
if not key then
100+
-- TODO: error
101+
return nil
102+
end
103+
form[vim.trim(key)] = vim.trim(value)
104+
end
105+
return form
106+
end
107+
108+
---@param content_type string?
75109
---@param body_node TSNode
76110
---@param source Source
77111
---@param context rest.Context
78-
---@return rest.Request.Body|nil
79-
function parser.parse_body(body_node, source, context)
112+
---@return rest.Request.Body?
113+
function parser.parse_body(content_type, body_node, source, context)
80114
local body = {}
81-
body.__TYPE = body_node:type():gsub("_%w+", "")
115+
local node_type = body_node:type()
82116
---@cast body rest.Request.Body
83-
if body.__TYPE == "json" then
117+
if node_type == "external_body" then
118+
body.__TYPE = "external"
119+
local path = assert(get_node_field_text(body_node, "path", source))
120+
if type(source) ~= "number" then
121+
logger.error("can't parse external body on non-existing http file")
122+
return
123+
end
124+
---@cast source integer
125+
local basepath = vim.api.nvim_buf_get_name(source):match("(.*)/.*")
126+
path = vim.fs.normalize(vim.fs.joinpath(basepath, path))
127+
body.data = {
128+
name = get_node_field_text(body_node, "name", source),
129+
path = path,
130+
}
131+
elseif node_type == "json_body" or content_type == "application/json" then
132+
body.__TYPE = "json"
84133
body.data = vim.trim(vim.treesitter.get_node_text(body_node, source))
85134
body.data = expand_variables(body.data, context)
86-
local ok, _ = pcall(vim.json.decode, body.data)
135+
local ok = validate_json(body.data)
87136
if not ok then
88137
logger.warn("invalid json: '" .. body.data .. "'")
89138
return nil
90139
end
91-
elseif body.__TYPE == "xml" then
140+
elseif node_type == "xml_body" or content_type == "application/xml" then
141+
body.__TYPE = "xml"
92142
body.data = vim.trim(vim.treesitter.get_node_text(body_node, source))
93143
body.data = expand_variables(body.data, context)
94-
local xml2lua = require("xml2lua")
95-
local handler = require("xmlhandler.tree"):new()
96-
local xml_parser = xml2lua.parser(handler)
97-
local ok = pcall(function (t) return xml_parser:parse(t) end, body.data)
144+
local ok = validate_xml(body.data)
98145
if not ok then
99146
logger.warn("invalid xml: '" .. body.data .. "'")
100147
return nil
101148
end
102-
elseif body.__TYPE == "form" then
103-
body.data = {}
104-
for pair, _ in body_node:iter_children() do
105-
if pair:type() == "query" then
106-
local key = assert(get_node_field_text(pair, "key", source))
107-
local value = assert(get_node_field_text(pair, "value", source))
108-
key = expand_variables(key, context)
109-
value = expand_variables(value, context)
110-
body.data[key] = value
149+
elseif node_type == "raw_body" then
150+
-- TODO: exclude comments from text
151+
local text = vim.treesitter.get_node_text(body_node, source)
152+
if content_type and vim.startswith(content_type, "application/x-www-form-urlencoded") then
153+
body.__TYPE = "form"
154+
body.data = parse_urlencoded_form(text)
155+
if not body.data then
156+
-- TODO: parsing urlencoded form failed
157+
return nil
111158
end
159+
else
160+
body.__TYPE = "raw"
161+
body.data = text
112162
end
113-
elseif body.__TYPE == "external" then
114-
local path = assert(get_node_field_text(body_node, "path", source))
115-
path = vim.fs.normalize(vim.fs.joinpath(vim.fn.expand("%:h"), path))
116-
body.data = {
117-
name = get_node_field_text(body_node, "name", source),
118-
path = path,
119-
}
120-
elseif body.__TYPE == "graphql" then
163+
elseif node_type == "multipart_form_data" then
164+
body.__TYPE = "multipart_form_data"
165+
-- TODO:
166+
logger.error("multipart form data is not supported yet")
167+
elseif node_type == "graphql_body" then
121168
logger.error("graphql body is not supported yet")
122169
end
123170
return body
@@ -273,15 +320,6 @@ function parser.parse(node, source, ctx)
273320
logger.error("request section doesn't have request node")
274321
return nil
275322
end
276-
local body
277-
local body_node = req_node:field("body")[1]
278-
if body_node then
279-
body = parser.parse_body(body_node, source, ctx)
280-
if not body then
281-
logger.error("parsing body failed")
282-
return nil
283-
end
284-
end
285323
local method = get_node_field_text(req_node, "method", source)
286324
if not method then
287325
logger.info("no method provided, falling back to 'GET'")
@@ -334,6 +372,23 @@ function parser.parse(node, source, ctx)
334372
url = host..url
335373
table.remove(headers["host"], 1)
336374
end
375+
376+
---@type string?
377+
local content_type
378+
if headers["content-type"] then
379+
content_type = headers["content-type"][1]:match("([^;]+)")
380+
end
381+
local body
382+
local body_node = req_node:field("body")[1]
383+
if body_node then
384+
body = parser.parse_body(content_type, body_node, source, ctx)
385+
if not body then
386+
logger.error("parsing body failed")
387+
vim.notify("[rest.nvim] parsing request body failed. See `:Rest logs` for more info.", vim.log.levels.ERROR)
388+
return nil
389+
end
390+
end
391+
337392
---@type rest.Request
338393
local req = {
339394
name = name,

lua/rest-nvim/request.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ local jar = require("rest-nvim.cookie_jar")
1212
local clients = require("rest-nvim.client")
1313

1414
---@class rest.Request.Body
15-
---@field __TYPE "json"|"xml"|"external"|"form"|"graphql"
15+
---@field __TYPE "json"|"xml"|"raw"|"graphql"|"multipart_form_data"|"form"|"external"
1616
---@field data any
1717

1818
---@class rest.Request

lua/rest-nvim/ui/result.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ local panes = {
7070
table.insert(lines, ("%s %d %s"):format(data.response.status.version, data.response.status.code, data.response.status.text))
7171
local content_type = data.response.headers["content-type"]
7272
table.insert(lines, "")
73-
table.insert(lines, "#+RES")
73+
table.insert(lines, "# @_RES")
7474
local body = vim.split(data.response.body, "\n")
7575
if content_type then
7676
local res_type = content_type[1]:match(".*/([^;]+)")
@@ -81,7 +81,7 @@ local panes = {
8181
end
8282
end
8383
vim.list_extend(lines, body)
84-
table.insert(lines, "#+END")
84+
table.insert(lines, "# @_END")
8585
else
8686
vim.list_extend(lines, { "", "# Loading..." })
8787
end

queries/http/highlights.scm

+11-11
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99
(variable_declaration
1010
name: (identifier) @variable)
1111

12-
; Parameters
13-
(query_param
14-
key: (_) @variable.parameter)
15-
1612
; Operators
17-
[
18-
"="
19-
"&"
20-
"@"
21-
"<"
22-
] @operator
13+
(comment
14+
"=" @operator)
15+
(variable_declaration
16+
"=" @operator)
17+
18+
; keywords
19+
(comment
20+
"@" @keyword
21+
name: (_) @keyword)
2322

2423
; Literals
2524
(request
@@ -37,7 +36,8 @@
3736
"}}"
3837
] @punctuation.bracket
3938

40-
":" @punctuation.delimiter
39+
(header
40+
":" @punctuation.delimiter)
4141

4242
; external JSON body
4343
(external_body

queries/http/injections.scm

+13-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@
99
((xml_body) @injection.content
1010
(#set! injection.language "xml"))
1111

12-
((graphql_body) @injection.content
12+
((graphql_data) @injection.content
1313
(#set! injection.language "graphql"))
1414

15-
; Lua scripting
15+
; Script (default to javascript)
1616
((script) @injection.content
1717
(#offset! @injection.content 0 2 0 -2)
18-
(#set! injection.language "lua"))
18+
(#set! injection.language "javascript"))
19+
20+
; Script with other languages
21+
((comment
22+
name: (_) @_name
23+
(#eq? @_name "lang")
24+
value: (_) @injection.language)
25+
.
26+
(_
27+
(script) @injection.content
28+
(#offset! @injection.content 0 2 0 -2)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// The request body is read from a file
2+
POST https://example.com:8080/api/html/post
3+
Content-Type: application/json
4+
5+
< ./input.json

spec/parser/http_parser_spec.lua

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---@module 'luassert'
2+
3+
require("spec.minimum_init")
4+
5+
local parser = require("rest-nvim.parser")
6+
local utils = require("rest-nvim.utils")
7+
8+
local function open(path)
9+
vim.cmd.edit(path)
10+
vim.cmd.source("ftplugin/http.lua")
11+
return 0
12+
end
13+
14+
describe("parser", function ()
15+
it("parse form-urlencoded body", function ()
16+
local source = [[
17+
POST https://ijhttp-examples.jetbrains.com/post
18+
Content-Type: application/x-www-form-urlencoded
19+
20+
key1 = value1 &
21+
key2 = value2 &
22+
key3 = value3 &
23+
key4 = value4 &
24+
key5 = value5
25+
]]
26+
local _, tree = utils.ts_parse_source(source)
27+
local req_node = assert(tree:root():child(0))
28+
assert.same({
29+
method = "POST",
30+
url = "https://ijhttp-examples.jetbrains.com/post",
31+
headers = {
32+
["content-type"] = { "application/x-www-form-urlencoded" },
33+
},
34+
cookies = {},
35+
handlers = {},
36+
body = {
37+
__TYPE = "form",
38+
data = {
39+
key1 = "value1",
40+
key2 = "value2",
41+
key3 = "value3",
42+
key4 = "value4",
43+
key5 = "value5",
44+
},
45+
},
46+
}, parser.parse(req_node, source))
47+
end)
48+
it("parse external body", function ()
49+
-- external body can be only sourced when
50+
local source = open("spec/examples/post_with_external_body.http")
51+
local _, tree = utils.ts_parse_source(source)
52+
local req_node = assert(tree:root():child(0))
53+
assert.same({
54+
method = "POST",
55+
url = "https://example.com:8080/api/html/post",
56+
headers = {
57+
["content-type"] = { "application/json" },
58+
},
59+
cookies = {},
60+
handlers = {},
61+
body = {
62+
__TYPE = "external",
63+
data = {
64+
path = "spec/examples/input.json"
65+
}
66+
},
67+
}, parser.parse(req_node, source))
68+
end)
69+
end)

0 commit comments

Comments
 (0)