Repository: pwntester/octo.nvim (lua)

Issue:
Support graphql inputs as lua tables
Following #782, I would like to better support graphql inputs in mutations. For instance, 

```lua
local mutation = [[
mutation($star_input: AddStarInput!) {
  addStar(input: $star_input) {
    starrable {
        id
    }
  }
}
]]

local addStarInput = {
    starrableId = "..."
}


gh.api.graphql {
    query = mutation, 
    fields = {
        star_input = addStarInput
    }
    ..., 
}
```

However, this can currently be done like this which might be more verbose: 

```lua
local mutation = [[
mutation($starrableId: ID!) {
  addStar(input: {
    starrableId: $starrableId
  }) {
    starrable {
        id
    }
  }
}
]]

local starrableId = "..."

gh.api.graphql {
    query = mutation, 
    fields = {
        starrableId = starrableId
    }
    ..., 
}
```

Reference: https://docs.github.com/en/graphql/reference/mutations#addstar

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

This allows for inputs for API calls

For example, the example from `gh api --help`:

```
# Post nested parameter read from a file
$ gh api gists -F 'files[myfile.txt][content]=@myfile.txt'
```

Would be: 

```lua
local gh = require "octo.gh"

gh.api.post {
  "/gists", 
  F = {
    files = {
      ["myfile.txt"] = {
        content = "@myfile.txt",
      }
    }
  }
}
```

#### Example

```lua
local gh = require "octo.gh"

local repo_id = gh.api.graphql {
  query = [[
  query($owner: String!, $name: String!) {
    repository(owner: $owner, name: $name) {
      id
    }
  }
  ]],
  F = { owner = vim.fn.input "Repo owner: ", name = vim.fn.input "Repo name: " },
  jq = ".data.repository.id",
  opts = { mode = "sync" },
}

gh.api.graphql {
  query = [[
  mutation($input: CreateIssueInput!) {
    createIssue(input: $input) {
      issue {
        id
        title
        body
      }
    }
  }
  ]],
  F = {
    input = {
      repositoryId = repo_id,
      title = vim.fn.input "Issue title: ",
      body = vim.fn.input "Issue body: ",
    }
  },
  jq = ".data.createIssue.issue",
  opts = { cb = gh.create_callback {} },
}
```

### Does this pull request fix one issue?

<!--If that, add "Fixes #xxxx" below in the next line. For etxample, Fixes #15. Otherwise, add "NONE" -->

Closes https://github.com/pwntester/octo.nvim/issues/787

Patch:
diff --git a/lua/octo/gh/init.lua b/lua/octo/gh/init.lua
index f40ebea6b..65acd7677 100644
--- a/lua/octo/gh/init.lua
+++ b/lua/octo/gh/init.lua
@@ -212,6 +212,25 @@ local create_flag = function(key)
   end
 end
 
+M.insert_input = function(args, flag, parameter, key, value)
+  if type(value) == "boolean" then
+    value = tostring(value)
+  end
+
+  if type(value) == "table" then
+    for k, v in pairs(value) do
+      local new_parameter = type(key) == "number" and parameter .. "[]" or parameter .. "[" .. key .. "]"
+      M.insert_input(args, flag, new_parameter, k, v)
+    end
+  elseif type(key) == "number" then
+    table.insert(args, flag)
+    table.insert(args, parameter .. "[]=" .. value)
+  else
+    table.insert(args, flag)
+    table.insert(args, parameter .. "[" .. key .. "]=" .. value)
+  end
+end
+
 ---Insert the options into the args table
 ---@param args table the arguments table
 ---@param options table the options to insert
@@ -233,9 +252,8 @@ M.insert_args = function(args, options, replace)
       if type(value) == "table" then
         for k, v in pairs(value) do
           if type(v) == "table" then
-            for _, vv in ipairs(v) do
-              table.insert(args, flag)
-              table.insert(args, k .. "[]=" .. vv)
+            for kk, vv in pairs(v) do
+              M.insert_input(args, flag, k, kk, vv)
             end
           elseif type(v) == "boolean" then
             if v then


Test patch:
diff --git a/lua/tests/plenary/gh_spec.lua b/lua/tests/plenary/gh_spec.lua
index 92dd11b0b..049eec3fb 100644
--- a/lua/tests/plenary/gh_spec.lua
+++ b/lua/tests/plenary/gh_spec.lua
@@ -1,6 +1,30 @@
 local gh = require "octo.gh"
 local eq = assert.are.same
 
+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("insert_args:", function()
   it("true booleans show up as flags", function()
     local args = {}
@@ -75,6 +99,20 @@ describe("insert_args:", function()
     }
     eq(args, expected)
   end)
+  it("integer values", function()
+    local args = {}
+    local opts = {
+      f = {
+        num_issues = 15,
+      },
+    }
+    gh.insert_args(args, opts)
+    local expected = {
+      "-f",
+      "num_issues=15",
+    }
+    eq(args, expected)
+  end)
   it("list of fields get brackets", function()
     local args = {}
     local opts = {
@@ -93,46 +131,107 @@ describe("insert_args:", function()
     }
     eq(args, expected)
   end)
-  it("integer values", function()
+  it("table of fields parsed correctly", function()
     local args = {}
     local opts = {
       f = {
-        num_issues = 15,
+        items = {
+          nested_list = { 1, 2, 3 },
+          nested_obj = { first = 1, second = 2 },
+          second = 2,
+        },
       },
     }
     gh.insert_args(args, opts)
     local expected = {
       "-f",
-      "num_issues=15",
+      "items[nested_list][]=1",
+      "-f",
+      "items[nested_list][]=2",
+      "-f",
+      "items[nested_list][]=3",
+      "-f",
+      "items[nested_obj][first]=1",
+      "-f",
+      "items[nested_obj][second]=2",
+      "-f",
+      "items[second]=2",
+    }
+    assert_tables_have_same_elements(args, expected)
+  end)
+  it("gh api --help schema example", function()
+    local args = {}
+    local opts = {
+      F = {
+        properties = {
+          {
+            property_name = "environment",
+            default_value = "production",
+            required = true,
+            allowed_values = {
+              "staging",
+              "production",
+            },
+          },
+        },
+      },
+    }
+    gh.insert_args(args, opts)
+    local expected = {
+      "-F",
+      "properties[][property_name]=environment",
+      "-F",
+      "properties[][required]=true",
+      "-F",
+      "properties[][default_value]=production",
+      "-F",
+      "properties[][allowed_values][]=staging",
+      "-F",
+      "properties[][allowed_values][]=production",
+    }
+    assert_tables_have_same_elements(args, expected)
+  end)
+  it("gh api --help gist example", function()
+    local args = {}
+    local opts = {
+      F = {
+        files = {
+          ["myfile.txt"] = {
+            content = "@myfile.txt",
+          },
+        },
+      },
+    }
+    gh.insert_args(args, opts)
+    local expected = {
+      "-F",
+      "files[myfile.txt][content]=@myfile.txt",
     }
     eq(args, expected)
   end)
+  it("gh api common use case", function()
+    local args = {}
+    local opts = {
+      f = { owner = "pwntester", repo = "octo.nvim", required = true },
+      F = { number = 1, total = 100 },
+    }
+    gh.insert_args(args, opts)
+    local expected = {
+      "-f",
+      "owner=pwntester",
+      "-f",
+      "repo=octo.nvim",
+      "-f",
+      "required=true",
+      "-F",
+      "number=1",
+      "-F",
+      "total=100",
+    }
+    assert_tables_have_same_elements(args, expected)
+  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, {})