Repository: pwntester/octo.nvim (lua)

Issue:
Helper to construct REST endpoints
Some ideas for the API:

```lua
local endpoint = construct_endpoint {
    path = "/repos/{owner}/{repo}/notifications/",
    params = {
        owner = "octocat",
        repo = "Hello-World"
    }
}
--- Or if reusability is needed
local endpoint = Endpoint("/repos/{owner}/{repo}/notifications/")

endpoint:construct {
    owner = "octocat",
    repo = "Hello-World"
}   
```

Related to #751

Pull request:
<!--  Thanks for submitting a pull request! Here are some tips for you:
1. Please make sure you have read and understood the contributing guidelines: https://github.com/pwntester/octo.nvim/blob/master/CONTRIBUTING.md
2. Please make sure the PR has a corresponding issue.
-->

### Describe what this PR does / why we need it

Closes #821

Use the `format` parameter to format a templated endpoint string

```lua
local gh = require "octo.gh"

local endpoint = "/repos/{owner}/{repo}/issues/{issue_number}"

local opts = { cb = gh.create_callback {} }

gh.api.get { 
  endpoint, 
  format = { owner = "pwntester", repo = "octo.nvim", issue_number = 1 },
  jq = "{ title, body, author: .user.login }",
  opts = opts,
}
```

However, the `format` parameter is not required. If it is not provided, the endpoint string is used as is (current behavior).

```lua
local gh = require "octo.gh"

local endpoint = "/repos/pwntester/octo.nvim/issues/1"

local opts = { cb = gh.create_callback {} }

gh.api.get { 
  endpoint, 
  jq = "{ title, body, author: .user.login }",
  opts = opts,
}
```

CC: @ldelossa


### Does this pull request fix one issue?

<!--If that, add "Fixes #xxxx" below in the next line. For example, Fixes #15. Otherwise, add "NONE" -->

### Describe how you did it


### Describe how to verify it


### Special notes for reviews

### Checklist

- [ ] Passing tests and linting standards
- [ ] Documentation updates in README.md and doc/octo.txt

Patch:
diff --git a/lua/octo/gh/init.lua b/lua/octo/gh/init.lua
index 75b37f161..72b3148e3 100644
--- a/lua/octo/gh/init.lua
+++ b/lua/octo/gh/init.lua
@@ -323,16 +323,50 @@ function M.api.graphql(opts)
   }
 end
 
-local rest = function(method, opts)
-  local run_opts = opts.opts or {}
+---Format the endpoint with the format table
+---@param endpoint string the endpoint to format
+---@param format table<key, value> the format table
+local format_endpoint = function(endpoint, format)
+  for key, value in pairs(format) do
+    endpoint = endpoint:gsub("{" .. key .. "}", value)
+  end
+  return endpoint
+end
+
+---@param method string the rest method
+---@param opts table the options for the rest command
+---@return table|nil
+M.create_rest_args = function(method, opts)
+  local format = opts.format or {}
+
+  local endpoint = opts[1]
+  if not endpoint then
+    return
+  end
+  endpoint = format_endpoint(endpoint, format)
+  opts[1] = endpoint
+
   local args = { "api" }
   if method ~= nil then
     table.insert(args, "--method")
     table.insert(args, method)
   end
 
+  opts.format = nil
   opts.opts = nil
-  args = M.insert_args(args, opts)
+  return M.insert_args(args, opts)
+end
+
+---Run a rest command
+local rest = function(method, opts)
+  local run_opts = opts.opts or {}
+
+  local args = M.create_rest_args(method, opts)
+  if not args then
+    local utils = require "octo.utils"
+    utils.error "Endpoint is required"
+    return
+  end
 
   run {
     args = args,


Test patch:
diff --git a/lua/tests/plenary/gh_spec.lua b/lua/tests/plenary/gh_spec.lua
index e81f909d8..92dd11b0b 100644
--- a/lua/tests/plenary/gh_spec.lua
+++ b/lua/tests/plenary/gh_spec.lua
@@ -109,6 +109,74 @@ describe("insert_args:", function()
   end)
 end)
 
+local tables_have_same_elements = function(t1, t2)
+  if #t1 ~= #t2 then
+    return false
+  end
+  for _, v in ipairs(t1) do
+    if not vim.tbl_contains(t2, v) then
+      return false
+    end
+  end
+  for _, v in ipairs(t2) do
+    if not vim.tbl_contains(t1, v) then
+      return false
+    end
+  end
+  return true
+end
+
+local assert_tables_have_same_elements = function(t1, t2)
+  assert(
+    tables_have_same_elements(t1, t2),
+    string.format("Expected tables to have the same elements:\n%s\n%s", vim.inspect(t1), vim.inspect(t2))
+  )
+end
+
+describe("REST API args", function()
+  it("no args", function()
+    local actual = gh.create_rest_args(nil, {})
+    eq(actual, nil)
+  end)
+  it("Endpoint is required", function()
+    local actual = gh.create_rest_args(nil, { format = { owner = "pwntester" }, json = "id", jq = ".id" })
+    eq(actual, nil)
+  end)
+  it("Returns table with untouched endpoint", function()
+    local actual = gh.create_rest_args("GET", {
+      "repos/pwntester/octo.nvim/pulls",
+      jq = ".[].number",
+      paginate = true,
+    })
+    assert_tables_have_same_elements(actual, {
+      "api",
+      "--method",
+      "GET",
+      "repos/pwntester/octo.nvim/pulls",
+      "--jq",
+      ".[].number",
+      "--paginate",
+    })
+  end)
+  it("Returns table with formated endpoint", function()
+    local actual = gh.create_rest_args("GET", {
+      "repos/{owner}/{name}/pulls",
+      format = { owner = "pwntester", name = "octo.nvim" },
+      jq = ".[].number",
+      paginate = true,
+    })
+    assert_tables_have_same_elements(actual, {
+      "api",
+      "--method",
+      "GET",
+      "repos/pwntester/octo.nvim/pulls",
+      "--jq",
+      ".[].number",
+      "--paginate",
+    })
+  end)
+end)
+
 describe("create_graphql_opts:", function()
   local query = "example query"
   local login = "pwntester"