Skip to content

Commit

Permalink
Add a javascript runner for running javascript before and after requests
Browse files Browse the repository at this point in the history
This implement a basic javascript runner.
* support Response API
* support set/get Request API variables
* support set/get client global variables
* support jsonPath query in javascript

Dev notes:
A intermediate file is created for each request in the script folder,
`last_javascript.mjs` is the last javascript file that was run.
This is useful for debugging. For instance you can run the file from the
command line `node --inspect-brk last_javascript.mjs` and then open up
`chrome://inspect` in chrome based browers and debug the javascript.
  • Loading branch information
mrloop committed Jan 23, 2025
1 parent 62606c3 commit 1be48dd
Show file tree
Hide file tree
Showing 5 changed files with 433 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
doc/tags
.repro/
repro*.lua
lua/rest-nvim/script/node_modules
lua/rest-nvim/script/last_javascript.mjs
170 changes: 170 additions & 0 deletions lua/rest-nvim/script/javascript.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
local script = {}
local logger = require("rest-nvim.logger")

local has_js = vim.fn.has('node')

if has_js == 0 then
print("Neovim was not built with JavaScript support")
end

local function cwd()
local current_script_path = debug.getinfo(1).source:match("@(.*)")
return current_script_path:match("(.*[/\\])") or "."
end

local function read_file(filename)
local file_to_open = cwd() .. "/" .. filename

local file = io.open(file_to_open, "r")
if file then
local file_content = file:read("*all")
file:close()
return file_content
else
print("Error: Could not open file " .. file_to_open)
end
end

local function write_file(filename, content)
local file_to_open = cwd() .. "/" .. filename
local file = io.open(file_to_open, "w")
if file then
file:write(content)
file:close()
else
print("Error: Could not open file " .. file_to_open)
end
return file_to_open
end

local function local_vars (ctx)
local ctx_vars = {}
for k, v in pairs(ctx.vars) do
ctx_vars[k] = v
end
for k, v in pairs(ctx.lv) do
ctx_vars[k] = v
end
return ctx_vars
end

local function create_prescript_env(ctx)
return {
_env = { cwd = cwd() },
request = { variables = local_vars(ctx) }
}
end

local function create_handler_env(ctx, res)
local env_vars = {}
-- can't do pairs(vim.env) instead need to use environ
for k, v in pairs(vim.fn.environ()) do
-- ignore values which will make escaping complicated
if not string.find(v, "[\\'\"`]") then
env_vars[k] = v
end
end

local response = res
-- TODO check mime type before parsing
local ok, decoded_body = pcall(vim.fn.json_decode, res.body)
if ok then
response = vim.deepcopy(res)
response.body = decoded_body
end

return {
_env = { cwd = cwd() },
client = { global = { data = env_vars } },
request = { variables = local_vars(ctx) },
response = response,
}
end

local function execute_cmd(cmd)
local handle = io.popen(cmd)
if handle then
local result = handle:read("*a")
handle:close()
return result
end
end

local js_str = read_file("javascript.mjs");

local function load_js(s, env)
local env_json = vim.fn.json_encode(env)

-- uncomment to load each time when developing
-- local js_str = read_file("javascript.mjs");

return function(...)
local js_code = string.format(js_str, env_json, s)

-- save to file so no need to escape quotes
local file_path = write_file('last_javascript.mjs', js_code)

local ok, result = pcall(function()
return execute_cmd("node " .. file_path)
end)
if not ok then
logger.error("JS execution error: " .. tostring(result))
return nil
end

return result
end
end

local function split_string_on_separator(multiline_str)
local before_separator = {}
local after_separator = {}
local found_separator = false

for line in multiline_str:gmatch("[^\r\n]+") do
if line == "-ENV-" then
found_separator = true
elseif found_separator then
table.insert(after_separator, line)
else
table.insert(before_separator, line)
end
end

return table.concat(before_separator), table.concat(after_separator)
end

local function update_local(ctx, env_json)
for key, value in pairs(env_json.request.variables) do
ctx:set_local(key, value)
end
end

local function update_global(env, env_json)
for key, value in pairs(env_json.client.global.data) do
env[key] = value
end
end

function script.load_pre_req_hook(s, ctx)
return function ()
local result = load_js(s, create_prescript_env(ctx))(s, ctx)
local logs, json_str = split_string_on_separator(result)
print(logs)
local env_json = vim.fn.json_decode(json_str)
update_local(ctx, env_json)
end
end

function script.load_post_req_hook(s, ctx)
return function(res)
local result = load_js(s, create_handler_env(ctx, res))(s, ctx)
local logs, json_str = split_string_on_separator(result)
print(logs)
local env_json = vim.fn.json_decode(json_str)
update_global(vim.env, env_json)
update_local(ctx, env_json)
end
end

return script
72 changes: 72 additions & 0 deletions lua/rest-nvim/script/javascript.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
try {
const ctx = JSON.parse(`%s`);

let jsonPath;
try {
let jsonPathModule = await import("jsonpath");
jsonPath = function (...args) {
return jsonPathModule.default.value(...args);
};
} catch (e) {
console.log(`jsonpath not found please install \`npm install --prefix ${ctx._env.cwd}\``);
}

let client;
if (ctx.client) {
// empty table json encoded as array by lua
ctx.client.global.data = Array.isArray(ctx.client.global.data) ? {} : ctx.client.global.data;

client = {
global: {
data: ctx.client.global.data,
get: function (key) {
return ctx.client.global.data[key];
},
set: function (key, value) {
ctx.client.global.data[key] = value;
},
},
};
}

let response;
if (ctx.response) {
//https://www.jetbrains.com/help/idea/http-response-reference.html
response = {
body: ctx.response.body,
headers: {
valueOf: function(key) {
return ctx.response.headers[key]?.[0]
},
valuesOf: function(key) {
return ctx.response.headers[key]
}
},
status: ctx.response.status.code,
contentType: {
mineType: ctx.response.headers["content-type"]?.[0]?.split(";")?.[0],
charset: ctx.response.headers["content-type"]?.[0].split(";")?.[0]
}
}
}

const request = {
variables: {
get: function (key) {
return ctx.request.variables[key];
},
set: function (key, value) {
ctx.request.variables[key] = value;
},
}
}

;(function(){ %s })();

console.log("-ENV-");
console.log(JSON.stringify(ctx));
} catch (error) {
console.log(error);
console.log("-ENV-");
console.log("{}");
}
Loading

0 comments on commit 1be48dd

Please sign in to comment.