From f621decc97a548911f05f29228bc0a53b54b1c46 Mon Sep 17 00:00:00 2001 From: Luca Bilke Date: Thu, 18 Apr 2024 16:47:37 +0200 Subject: [PATCH] first work on API --- .editorconfig | 8 ++ .luacheckrc | 9 ++ LICENSE | 15 +++ README.md | 22 ++++ lua/transformer/api/anthropic.lua | 3 + lua/transformer/init.lua | 27 +++++ lua/transformer/util/init.lua | 177 ++++++++++++++++++++++++++++++ 7 files changed, 261 insertions(+) create mode 100644 .editorconfig create mode 100644 .luacheckrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lua/transformer/api/anthropic.lua create mode 100644 lua/transformer/init.lua create mode 100644 lua/transformer/util/init.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..72fb8ad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.lua] +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..dbab055 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,9 @@ +ignore = { + "631", -- max_line_length +} +read_globals = { + "vim", + "describe", + "it", + "assert" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..997df1b --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright 2024 Luca Bilke + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..297dae7 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# transformer.nvim + +## Development + +### Run tests + +Running tests requires [plenary.nvim][plenary] to be checked out in the parent +directory of *this* repository. You can then run: + +```bash +nvim --headless --noplugin -u tests/minimal.vim \ + -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}" +``` + +Or if you want to run a single test file: + +```bash +nvim --headless --noplugin -u tests/minimal.vim \ + -c "PlenaryBustedDirectory tests/path_to_file.lua {minimal_init = 'tests/minimal.vim'}" +``` + +[plenary]: https://github.com/nvim-lua/plenary.nvim diff --git a/lua/transformer/api/anthropic.lua b/lua/transformer/api/anthropic.lua new file mode 100644 index 0000000..5044d6f --- /dev/null +++ b/lua/transformer/api/anthropic.lua @@ -0,0 +1,3 @@ +local M = {} + +return M diff --git a/lua/transformer/init.lua b/lua/transformer/init.lua new file mode 100644 index 0000000..e17d7ad --- /dev/null +++ b/lua/transformer/init.lua @@ -0,0 +1,27 @@ +local M = {} + +---@alias provider_name "anthropic" +---@alias hook_name "request_started" | "request_finished" +M.opts = { + anthropic = { + hooks = { + request_started = nil, + request_finished = nil, + }, + api_url = nil, -- TODO: set default + token = nil, + }, +} + +M.state = { + anthropic = { + num_callbacks = 0, + spinner_index = 0, + }, +} + +function M.setup(opts) + M.opts = vim.tbl_deep_extend("force", M._G.opts, opts) +end + +return M diff --git a/lua/transformer/util/init.lua b/lua/transformer/util/init.lua new file mode 100644 index 0000000..32975ca --- /dev/null +++ b/lua/transformer/util/init.lua @@ -0,0 +1,177 @@ +local opts = require("transformer").opts +local state = require("transformer").state + +---@alias curl_callback fun(deserialized: table) + +---@class curl_opts +---@field url string The url to make the request to +---@field query table url query, append after the url +---@field body string | table The request body, can be a path to a file +---@field auth string | table Basic request auth, 'user:pass', or {"user", "pass"} +---@field form table request form +---@field raw table any additonal curl args +---@field dry_run boolean whether to return the args to be ran through curl +---@field output string download destination filepath +---@field timeout integer request timeout in mseconds +---@field http_version "HTTP/0.9" | "HTTP/1.0" | "HTTP/1.1" | "HTTP/2" | "HTTP/3" HTTP version to use +---@field proxy string proxy url in this format: [protocol://]host[:port] +---@field insecure boolean Allow insecure server connections + +---@class curl_response +---@field exit integer The shell process exit code +---@field status integer The https response status +---@field headers table The https response headers +---@field body string The http response body + +local M = {} + +local spinner = { + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", +} + +--- TODO: Document this function +--- +---@param provider provider_name +---@return string spinner +function M.get_spinner(provider) + local p = state[provider] + + if p.num_callbacks == 0 then + return "" + end + + p.spinner_index = p.spinner_index + 1 + return spinner[p.spinner_index % #spinner + 1] +end + +--- TODO: Document this function +--- +---@param provider provider_name +---@param hook hook_name +local function run_hook(provider, hook) + local p = state[provider] + + if hook == "request_started" then + p.num_callbacks = p.num_callbacks + 1 + elseif hook == "request_finished" then + p.num_callbacks = p.num_callbacks - 1 + end + + pcall(M.opts[provider].hooks[hook]) +end + +--- # Curl exit code test +--- +--- Tests the curl exit code and notifies the user if something went wrong. +---@param exit_code integer +---@return boolean +function M.test_code(exit_code) + if exit_code == 127 then + vim.notify("transformer.util couldn't find curl binary in path", vim.log.levels.ERROR) + return false + elseif exit_code ~= 0 then + vim.notify("transformer.util unkown curl exit code: " .. exit_code, vim.log.levels.ERROR) + return false + end + return true +end + +--- TODO: Document this function +--- +---@param provider provider_name +---@return table? headers +local function make_headers(provider) + local headers = { Content_Type = "application/json" } + + if not opts[provider].token then + vim.notify("transformer.util no API key provided for " .. provider, vim.log.levels.ERROR) + return headers + end + + return vim.tbl_deep_extend("force", headers, { Authorization = "Bearer " .. opts[provider].token }) +end + +--- TODO: Document this function +--- +---@param provider provider_name +---@param response curl_response +---@param callback curl_callback +local function curl_callback(provider, response, callback) + if response.status ~= 200 then + response.body = response.body:gsub("%s+", " ") + print("Error: " .. response.status .. " " .. response.body) + return + end + + if response.body == nil or response.body == "" then + vim.notify("transformer.util empty response body", vim.log.levels.ERROR) + return + end + + vim.schedule_wrap(function(body) + local ok, result = pcall(vim.fn.json_decode, body) + if ok then + callback(result) + else + vim.notify("transformer.util failed to decode json: " .. result, vim.log.levels.ERROR) + end + end)(response.body) + + run_hook(provider, "request_finished") +end + +--- TODO: Document this function +--- +---@param provider provider_name +---@param payload table +---@param callback fun(decoded: table): nil +function M.call(provider, payload, callback) + local payload_str = vim.fn.json_encode(payload) + local url = opts[provider].api_url + local headers = make_headers(provider) + + if type(opts[provider].hooks.request_started) == "function" then + opts[provider].hooks.request_started() + else + vim.notify("transformer.util non-function variable set as hook for " .. provider, vim.log.levels.WARNING) + end + + require("plenary.curl").post(url, { + body = payload_str, + headers = headers, + callback = function(response) + curl_callback(provider, response, callback) + end, + on_error = function(err) + vim.notify("transformer.util curl error from: " .. err.message, vim.log.levels.WARNING) + run_hook(provider, "request_finished") + end, + }) +end + +return M