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