1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
package cmd
import (
"encoding/json"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
)
var (
apiFields []string
apiRawFields []string
)
func init() {
apiCmd := &cobra.Command{
Use: "api <method> <path>",
Short: "Make an authenticated request to an arbitrary API endpoint",
Long: `Low-level escape hatch, like "gh api". METHOD is GET, POST, PATCH, PUT, DELETE.
PATH is relative to /api/v1 (a leading /api/v1 or / is optional).
Fields (--field/-f) are added as query parameters for GET/HEAD and as a JSON
body otherwise. --field values are type-inferred (true/false/null/numbers);
use --raw-field/-F to force a string. The JSON response is printed to stdout.
Examples:
rickub api GET /user
rickub api GET search/repos -f q=api
rickub api POST /repos -f name=demo -f visibility=public
rickub api PATCH repos/me/demo -F description="hello world"`,
Args: cobra.ExactArgs(2),
RunE: runAPI,
}
apiCmd.Flags().StringArrayVarP(&apiFields, "field", "f", nil, "typed field key=value (repeatable)")
apiCmd.Flags().StringArrayVarP(&apiRawFields, "raw-field", "F", nil, "string field key=value (repeatable)")
rootCmd.AddCommand(apiCmd)
}
func runAPI(cmd *cobra.Command, args []string) error {
client, err := newClient()
if err != nil {
return err
}
method := strings.ToUpper(args[0])
path := normalizeAPIPath(args[1])
fields := map[string]any{}
for _, f := range apiRawFields {
k, v, ok := splitKV(f)
if !ok {
return fmt.Errorf("invalid --raw-field %q (want key=value)", f)
}
fields[k] = v
}
for _, f := range apiFields {
k, v, ok := splitKV(f)
if !ok {
return fmt.Errorf("invalid --field %q (want key=value)", f)
}
fields[k] = inferType(v)
}
var query url.Values
var body any
isRead := method == "GET" || method == "HEAD"
if len(fields) > 0 {
if isRead {
query = url.Values{}
for k, v := range fields {
query.Set(k, fmt.Sprintf("%v", v))
}
} else {
body = fields
}
}
data, _, err := client.RawJSON(cmd.Context(), method, path, query, body)
if err != nil {
return err
}
// Pretty-print JSON when possible, else emit raw bytes.
var pretty any
if len(strings.TrimSpace(string(data))) > 0 && json.Unmarshal(data, &pretty) == nil {
return printJSON(cmd.OutOrStdout(), pretty)
}
_, err = os.Stdout.Write(data)
return err
}
func normalizeAPIPath(p string) string {
p = strings.TrimPrefix(p, "/api/v1")
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return p
}
func splitKV(s string) (string, string, bool) {
i := strings.IndexByte(s, '=')
if i < 0 {
return "", "", false
}
return s[:i], s[i+1:], true
}
// inferType coerces common scalar spellings so JSON bodies get proper types.
func inferType(v string) any {
switch v {
case "true":
return true
case "false":
return false
case "null":
return nil
}
if n, err := strconv.Atoi(v); err == nil {
return n
}
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
return v
}
|