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
Initial import of the rickub CLI as a standalone public project 1a1d430Unverified · on main · Olivier Girardot · 7h ago
api.go · 127 lines · 2.9 KBGo Blame HistoryRaw
  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
}