Skip to content

Commit

Permalink
feat(from_curl): import HTTP spec from curl command line (#186)
Browse files Browse the repository at this point in the history
* feat(from_curl): import HTTP spec from curl command line

Curl is a standard way of specifying the API requests. It is a common
format in scripts, API docs and browsers supports Copy as cURL in the
network tools as well.

This commit adds

1. shlex-lua source code into project
2. code parsing the curl command line and generating the Request table
3. code appending the Request into current buffer

* Split the curl parsing into new module

use nvim_put to print the HTTP spec

* Improve curl command line parsing

1. supports short and long options now
2. supports HTTP version specification
3. now is omited if not specified on a command line

* Include documentation

* Problem: url can appear everywhere in a command line

Solution: when not parsing any particular command, consider the first
argument matching `^http(s)?://` as url. This fixes an export from
Firefox for example.

* Problem: --data-raw not supported

And it is used by Mozilla Firefox cURL export. According man curl
the default content encoding is application/x-www-form-urlencoded, so
read this too.

* Support --json

This is neat shortcut for

```
--data [arg]
--header "Content-Type: application/json"
--header "Accept: application/json"
```

* Problem: header parsing was broken

Solution: add a proper cut method - similar to strings.Cut from Go,
which splits the string into two parts and use remove_extra_space
allowing form `--header 'Key: Value` to become `"Key"`, `"Value"`.

* Problem: support http/https scheme only

Solution: match all valid schemes if they ends with :// - curl supports
more than HTTP anyway.

* refactor(from-curl)

* chore(cleanup)

* feat(from-curl): multi-line curl support

* refactor(from-curl): put original curl command

---------

Co-authored-by: Marco Kellershoff <[email protected]>
  • Loading branch information
vyskocilm and gorillamoe authored Sep 1, 2024
1 parent e406456 commit 8a532b1
Show file tree
Hide file tree
Showing 7 changed files with 584 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/docs/usage/public-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ It's default contents can be configured via the
`require('kulala').copy()` copies the current request
(as cURL command) to the clipboard.

### from_curl

`require('kulala').from_curl()` parse the cURL command from the clipboard and
write the HTTP spec into current buffer. It is useful for importing requests
from other tools like browsers.

### close

`require('kulala').close()` closes the kulala window and also the current buffer.
Expand Down
4 changes: 4 additions & 0 deletions lua/kulala/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ M.show_stats = function()
UI:show_stats()
end

M.from_curl = function()
UI:from_curl()
end

M.version = function()
local neovim_version = vim.fn.execute("version") or "Unknown"
Logger.info("Kulala version: " .. GLOBALS.VERSION .. "\n\n" .. "Neovim version: " .. neovim_version)
Expand Down
336 changes: 336 additions & 0 deletions lua/kulala/lib/shlex/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
-- All credits due to the original author of this code
-- https://github.com/BodneyC/shlex-lua/
--
-- It has been modified to work with Neovim and removed some continue statements
-- that could be also breaks, idk about performance but it works and
-- doesn't break stylua.
--
-- The original code has no license, so I'm assuming it's public domain
-- The author states that:
--
-- It's a couple files you can drop into your project
-- (or even your Neovim config (which is why I wrote it))
-- if you need to parse a shell command.

local M = {}

local function some(o)
return o and #o > 0
end

local function none(o)
return not some(o)
end

M.shlex = {
whitespace = " \t\r\n",
whitespace_split = false,
quotes = [['"]],
escape = [[\]],
escapedquotes = '"',
state = " ",
pushback = {},
lineno = 1,
debug = 0,
token = "",
commenters = "#",
wordchars = "abcdfeghijklmnopqrstuvwxyz" .. "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",
}
M.shlex.__index = M.shlex

local sr = require("kulala.lib.shlex.stringreader")

function M.shlex:create(str, posix, punctuation_chars)
local o = {}
setmetatable(o, M.shlex)

o.sr = sr(str or "")

if not posix then
o.eof = ""
end

o.posix = posix == true
if o.posix then
o.wordchars = o.wordchars
.. "ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
.. "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ"
end

if punctuation_chars then
punctuation_chars = "();<>|&"
else
punctuation_chars = ""
end

o.punctuation_chars = punctuation_chars

if punctuation_chars then
o._pushback_chars = {}
o.wordchars = o.wordchars .. "~-./*?="
for i = 1, #o.punctuation_chars do
local c = o.punctuation_chars:sub(i, i)
o.wordchars:gsub(c, "", 1, true)
end
end

return o
end

function M.shlex:push_token(tok)
table.insert(self.pushback, tok)
end

function M.shlex:read_token()
local quoted = false
local escapedstate = " "
local nextchar

while true do
if some(self.punctuation_chars) and some(self._pushback_chars) then
nextchar = table.remove(self._pushback_chars)
else
nextchar = self.sr:read(1)
end

if nextchar == "\n" then
self.lineno = self.lineno + 1
end

if self.debug >= 3 then
print("shlex: in state '" .. (self.state or "nil") .. "' I see character: '" .. (nextchar or "nil") .. "'")
end

if none(self.state) then
self.token = ""
break
elseif self.state == " " then
if none(nextchar) then
self.state = nil
break
elseif self.whitespace:find(nextchar, 1, true) then
if self.debug >= 2 then
print("shlex: I see whitespace in whitespace state")
end
if some(self.token) or (self.posix and quoted) then
break
else
break
end
elseif self.commenters:find(nextchar, 1, true) then
self.sr:readline()
self.lineno = self.lineno + 1
elseif self.posix and self.escape:find(nextchar, 1, true) then
escapedstate = "a"
self.state = nextchar
elseif self.wordchars:find(nextchar, 1, true) then
self.token = nextchar
self.state = "a"
elseif self.punctuation_chars:find(nextchar, 1, true) then
self.token = nextchar
self.state = "c"
elseif self.quotes:find(nextchar, 1, true) then
if not self.posix then
self.token = nextchar
end
self.state = nextchar
elseif self.whitespace_split then
self.token = nextchar
self.state = "a"
else
self.token = nextchar
if some(self.token) or (self.posix and quoted) then
break
else
break
end
end
elseif self.quotes:find(self.state, 1, true) then
quoted = true
if none(nextchar) then
if self.debug >= 2 then
print("shlex: I see EOF in quotes state")
end
error("no closing quotation")
end
if nextchar == self.state then
if not self.posix then
self.token = self.token .. nextchar
self.state = " "
break
else
self.state = "a"
end
elseif self.posix and self.escape:find(nextchar, 1, true) and self.escapedquotes:find(self.state, 1, true) then
escapedstate = self.state
self.state = nextchar
else
self.token = self.token .. nextchar
end
elseif self.escape:find(self.state, 1, true) then
if none(nextchar) then
if self.debug >= 2 then
print("shlex: I see EOF in escape state")
end
error("no escaped character")
end
if self.quotes:find(escapedstate, 1, true) and nextchar ~= self.state and nextchar ~= escapedstate then
self.token = self.token .. self.state
end
self.token = self.token .. nextchar
self.state = escapedstate
elseif self.state == "a" or self.state == "c" then
if none(nextchar) then
self.state = nil
break
elseif self.whitespace:find(nextchar, 1, true) then
if self.debug >= 2 then
print("shlex: I see whitespace in word state")
end
self.state = " "
if some(self.token) or (self.posix and quoted) then
break
else
break
end
elseif self.commenters:find(nextchar, 1, true) then
self.sr:readline()
self.lineno = self.lineno + 1
if self.posix then
self.state = " "
if some(self.token) or (self.posix and quoted) then
break
else
break
end
end
elseif self.state == "c" then
if self.punctuation_chars:find(nextchar, 1, true) then
self.token = self.token .. nextchar
else
if not self.whitespace:find(nextchar, 1, true) then
table.insert(self._pushback_chars, nextchar)
end
self.state = " "
break
end
elseif self.posix and self.quotes:find(nextchar, 1, true) then
self.state = nextchar
elseif self.posix and self.escape:find(nextchar, 1, true) then
escapedstate = "a"
self.state = nextchar
elseif
self.wordchars:find(nextchar, 1, true)
or self.quotes:find(nextchar, 1, true)
or (self.whitespace_split and not self.punctuation_chars:find(nextchar, 1, true))
then
self.token = self.token .. nextchar
else
if some(self.punctuation_chars) then
table.insert(self._pushback_chars, nextchar)
else
table.insert(self.pushback, nextchar)
end
self.state = " "
if some(self.token) or (self.posix and quoted) then
break
else
break
end
end
end
end

local result = self.token
self.token = ""
if self.posix and not quoted and result == "" then
result = nil
end
if result and result:find("^%s*$") then
result = self:read_token()
end
if self.debug > 1 then
if result then
print("shlex: raw token=" .. result)
else
print("shlex: raw token=EOF")
end
end
return result
end

function M.shlex:next()
if some(self.pushback) then
return table.remove(self.pushback)
end
local raw = self:read_token()
return raw
end

function M.shlex:list()
local parts = {}
while true do
local next = self:next()
if next == self.eof or next == nil then
break
end
table.insert(parts, next)
end
return parts
end

setmetatable(M.shlex, {
__call = M.shlex.create,
})

function M.split(str, comments, posix)
if not str then
str = ""
end
if type(posix) == "nil" then
posix = true
end
local lex = M.shlex(str)
lex.posix = posix
if comments == false then
lex.commenters = ""
end
return lex:list()
end

function M.join(parts)
local ret = ""
for idx, part in ipairs(parts) do
ret = ret .. M.quote(part)
if idx ~= #parts then
ret = ret .. " "
end
end
return ret
end

M._unsafe = "^@%+=:,./-"

function M.quote(s)
if none(s) then
return [['']]
end
local found = false
if s:find("%w") then
found = true
else
for i = 1, #s do
local c = s:sub(i, i)
if M._unsafe:find(c, 1, true) then
found = true
break
end
end
end
if not found then
return s
end
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
end

return M
Loading

0 comments on commit 8a532b1

Please sign in to comment.