mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
253 lines
5.8 KiB
Go
253 lines
5.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmdutil
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/client"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/util"
|
|
)
|
|
|
|
// DryRunAPICall describes a single API call in dry-run output.
|
|
type DryRunAPICall struct {
|
|
Desc string `json:"desc,omitempty"`
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Params map[string]interface{} `json:"params,omitempty"`
|
|
Body interface{} `json:"body,omitempty"`
|
|
}
|
|
|
|
// DryRunAPI is the builder and result type for dry-run output.
|
|
// URL templates use :param placeholders; Set stores actual values; MarshalJSON and Format resolve them.
|
|
type DryRunAPI struct {
|
|
desc string
|
|
calls []DryRunAPICall
|
|
extra map[string]interface{}
|
|
}
|
|
|
|
func NewDryRunAPI() *DryRunAPI {
|
|
return &DryRunAPI{extra: map[string]interface{}{}}
|
|
}
|
|
|
|
// --- HTTP method builders (add a call, return self for chaining) ---
|
|
|
|
func (d *DryRunAPI) GET(url string) *DryRunAPI {
|
|
d.calls = append(d.calls, DryRunAPICall{Method: "GET", URL: url})
|
|
return d
|
|
}
|
|
|
|
func (d *DryRunAPI) POST(url string) *DryRunAPI {
|
|
d.calls = append(d.calls, DryRunAPICall{Method: "POST", URL: url})
|
|
return d
|
|
}
|
|
|
|
func (d *DryRunAPI) PUT(url string) *DryRunAPI {
|
|
d.calls = append(d.calls, DryRunAPICall{Method: "PUT", URL: url})
|
|
return d
|
|
}
|
|
|
|
func (d *DryRunAPI) DELETE(url string) *DryRunAPI {
|
|
d.calls = append(d.calls, DryRunAPICall{Method: "DELETE", URL: url})
|
|
return d
|
|
}
|
|
|
|
func (d *DryRunAPI) PATCH(url string) *DryRunAPI {
|
|
d.calls = append(d.calls, DryRunAPICall{Method: "PATCH", URL: url})
|
|
return d
|
|
}
|
|
|
|
// Body sets the request body on the last added call.
|
|
func (d *DryRunAPI) Body(body interface{}) *DryRunAPI {
|
|
if n := len(d.calls); n > 0 {
|
|
d.calls[n-1].Body = body
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Params sets query parameters on the last added call.
|
|
func (d *DryRunAPI) Params(params map[string]interface{}) *DryRunAPI {
|
|
if n := len(d.calls); n > 0 {
|
|
d.calls[n-1].Params = params
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Desc sets a description on the last added call.
|
|
// If no calls exist yet, sets the top-level description.
|
|
func (d *DryRunAPI) Desc(desc string) *DryRunAPI {
|
|
if n := len(d.calls); n > 0 {
|
|
d.calls[n-1].Desc = desc
|
|
} else {
|
|
d.desc = desc
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Set adds an extra context field. Values are also used to resolve :key placeholders in URLs.
|
|
func (d *DryRunAPI) Set(key string, value interface{}) *DryRunAPI {
|
|
d.extra[key] = value
|
|
return d
|
|
}
|
|
|
|
// resolveURL replaces :key placeholders in url with path-escaped values from extra.
|
|
func (d *DryRunAPI) resolveURL(rawURL string) string {
|
|
for k, v := range d.extra {
|
|
rawURL = strings.ReplaceAll(rawURL, ":"+k, url.PathEscape(fmt.Sprintf("%v", v)))
|
|
}
|
|
return rawURL
|
|
}
|
|
|
|
// MarshalJSON serializes as {"description": "...", "api": [...calls with resolved URLs], ...extra}.
|
|
func (d *DryRunAPI) MarshalJSON() ([]byte, error) {
|
|
resolved := make([]DryRunAPICall, len(d.calls))
|
|
for i, c := range d.calls {
|
|
resolved[i] = DryRunAPICall{
|
|
Desc: c.Desc,
|
|
Method: c.Method,
|
|
URL: d.resolveURL(c.URL),
|
|
Params: c.Params,
|
|
Body: c.Body,
|
|
}
|
|
}
|
|
m := make(map[string]interface{}, len(d.extra)+2)
|
|
if d.desc != "" {
|
|
m["description"] = d.desc
|
|
}
|
|
m["api"] = resolved
|
|
for k, v := range d.extra {
|
|
m[k] = v
|
|
}
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
// Format renders the dry-run output as plain text for AI/human consumption.
|
|
func (d *DryRunAPI) Format() string {
|
|
var b strings.Builder
|
|
|
|
if d.desc != "" {
|
|
b.WriteString("# ")
|
|
b.WriteString(d.desc)
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
for i, c := range d.calls {
|
|
if i > 0 || d.desc != "" {
|
|
b.WriteByte('\n')
|
|
}
|
|
if c.Desc != "" {
|
|
b.WriteString("# ")
|
|
b.WriteString(c.Desc)
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
u := d.resolveURL(c.URL)
|
|
if len(c.Params) > 0 {
|
|
u += "?" + encodeParams(c.Params)
|
|
}
|
|
|
|
method := c.Method
|
|
if method == "" {
|
|
method = "GET"
|
|
}
|
|
b.WriteString(method)
|
|
b.WriteByte(' ')
|
|
b.WriteString(u)
|
|
b.WriteByte('\n')
|
|
|
|
if !util.IsNil(c.Body) {
|
|
j, _ := json.Marshal(c.Body)
|
|
b.WriteString(" ")
|
|
b.Write(j)
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
if len(d.calls) == 0 && len(d.extra) > 0 {
|
|
if d.desc != "" {
|
|
b.WriteByte('\n')
|
|
}
|
|
keys := make([]string, 0, len(d.extra))
|
|
for k := range d.extra {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
sv := dryRunFormatValue(d.extra[k])
|
|
if sv == "" {
|
|
continue
|
|
}
|
|
b.WriteString(k)
|
|
b.WriteString(": ")
|
|
b.WriteString(sv)
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func dryRunFormatValue(v interface{}) string {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return val
|
|
case nil:
|
|
return ""
|
|
default:
|
|
j, _ := json.Marshal(val)
|
|
return string(j)
|
|
}
|
|
}
|
|
|
|
func encodeParams(params map[string]interface{}) string {
|
|
vals := url.Values{}
|
|
for k, v := range params {
|
|
vals.Set(k, fmt.Sprintf("%v", v))
|
|
}
|
|
return vals.Encode()
|
|
}
|
|
|
|
// PrintDryRun outputs a standardised dry-run summary using DryRunAPI.
|
|
// When format is "pretty", outputs human-readable text; otherwise JSON.
|
|
func PrintDryRun(w io.Writer, request client.RawApiRequest, config *core.CliConfig, format string) error {
|
|
dr := NewDryRunAPI()
|
|
switch request.Method {
|
|
case "POST":
|
|
dr.POST(request.URL)
|
|
case "PUT":
|
|
dr.PUT(request.URL)
|
|
case "PATCH":
|
|
dr.PATCH(request.URL)
|
|
case "DELETE":
|
|
dr.DELETE(request.URL)
|
|
default:
|
|
dr.GET(request.URL)
|
|
}
|
|
if len(request.Params) > 0 {
|
|
dr.Params(request.Params)
|
|
}
|
|
if !util.IsNil(request.Data) {
|
|
dr.Body(request.Data)
|
|
}
|
|
dr.Set("as", string(request.As))
|
|
dr.Set("appId", config.AppID)
|
|
if config.UserOpenId != "" {
|
|
dr.Set("userOpenId", config.UserOpenId)
|
|
}
|
|
fmt.Fprintln(w, "=== Dry Run ===")
|
|
if format == "pretty" {
|
|
fmt.Fprint(w, dr.Format())
|
|
} else {
|
|
output.PrintJson(w, dr)
|
|
}
|
|
return nil
|
|
}
|