Skip to content

Commit c090b75

Browse files
authored
Yapi send (#103)
* fix install script * yapi send * yapi send examples
1 parent 4a578db commit c090b75

6 files changed

Lines changed: 196 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DA
99

1010
install: build
1111
@echo "Installing yapi to $$(go env GOPATH)/bin..."
12+
@mkdir -p $$(go env GOPATH)/bin
1213
@cp ./cli/bin/yapi $$(go env GOPATH)/bin/yapi
1314
@codesign --sign - --force $$(go env GOPATH)/bin/yapi 2>/dev/null || true
1415
@echo "Done! Ensure $$(go env GOPATH)/bin is in your PATH."

cli/cmd/yapi/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func main() {
9595
Stress: app.stressE,
9696
About: aboutE,
9797
Import: importE,
98+
Send: app.sendE,
9899
}
99100

100101
rootCmd := commands.BuildRoot(cfg, handlers)

cli/cmd/yapi/send.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"yapi.run/cli/internal/constants"
10+
"yapi.run/cli/internal/domain"
11+
"yapi.run/cli/internal/executor"
12+
"yapi.run/cli/internal/output"
13+
"yapi.run/cli/internal/runner"
14+
)
15+
16+
func (app *rootCommand) sendE(cmd *cobra.Command, args []string) error {
17+
url := args[0]
18+
19+
var body string
20+
if len(args) > 1 {
21+
body = args[1]
22+
}
23+
24+
method, _ := cmd.Flags().GetString("method")
25+
headers, _ := cmd.Flags().GetStringSlice("header")
26+
verbose, _ := cmd.Flags().GetBool("verbose")
27+
jsonOutput, _ := cmd.Flags().GetBool("json")
28+
jqFilter, _ := cmd.Flags().GetString("jq")
29+
30+
log := NewLogger(verbose)
31+
32+
// Detect transport from URL scheme
33+
transport := domain.DetectTransport(url, false)
34+
35+
// Default method: POST if body provided, GET otherwise (HTTP only)
36+
if method == "" {
37+
if transport == constants.TransportHTTP {
38+
if body != "" {
39+
method = constants.MethodPOST
40+
} else {
41+
method = constants.MethodGET
42+
}
43+
}
44+
} else {
45+
method = constants.CanonicalizeMethod(method)
46+
}
47+
48+
// Build request
49+
req := &domain.Request{
50+
URL: url,
51+
Method: method,
52+
Headers: make(map[string]string),
53+
Metadata: make(map[string]string),
54+
}
55+
56+
// Parse headers from -H flags
57+
for _, h := range headers {
58+
parts := strings.SplitN(h, ":", 2)
59+
if len(parts) != 2 {
60+
return fmt.Errorf("invalid header format %q: expected 'Key: Value'", h)
61+
}
62+
req.Headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
63+
}
64+
65+
// Set body
66+
if body != "" {
67+
req.Body = strings.NewReader(body)
68+
69+
// For HTTP, default to JSON content type if body looks like JSON and no content-type set
70+
if transport == constants.TransportHTTP {
71+
if _, hasContentType := req.Headers["Content-Type"]; !hasContentType {
72+
trimmed := strings.TrimSpace(body)
73+
if (strings.HasPrefix(trimmed, "{") && strings.HasSuffix(trimmed, "}")) ||
74+
(strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]")) {
75+
req.Headers["Content-Type"] = "application/json"
76+
}
77+
}
78+
}
79+
}
80+
81+
// Set transport metadata
82+
req.Metadata["transport"] = transport
83+
if app.insecure {
84+
req.Metadata["insecure"] = "true"
85+
}
86+
87+
// TCP-specific metadata defaults
88+
if transport == constants.TransportTCP {
89+
req.Metadata["data"] = body
90+
req.Metadata["encoding"] = "text"
91+
req.Metadata["read_timeout"] = "5"
92+
req.Metadata["idle_timeout"] = "500"
93+
req.Metadata["close_after_send"] = "true"
94+
}
95+
96+
if jqFilter != "" {
97+
req.Metadata["jq_filter"] = jqFilter
98+
}
99+
100+
log.Verbose("Transport: %s", transport)
101+
log.Request(method, url, req.Headers, body)
102+
103+
// Get executor
104+
exec, err := executor.GetTransport(transport, app.httpClient)
105+
if err != nil {
106+
return err
107+
}
108+
109+
// Execute
110+
opts := runner.Options{
111+
NoColor: app.noColor,
112+
BinaryOutput: app.binaryOutput,
113+
Insecure: app.insecure,
114+
}
115+
116+
result, err := runner.Run(context.Background(), exec, req, nil, opts)
117+
if err != nil {
118+
return err
119+
}
120+
121+
log.Response(result.StatusCode, result.Headers, result.Duration, result.BodyBytes)
122+
123+
if jsonOutput {
124+
return output.PrintJSON(output.JSONParams{
125+
Result: result,
126+
})
127+
}
128+
129+
return app.printResult(result, nil, "send", printResultOptions{skipMeta: verbose})
130+
}

cli/internal/cli/commands/commands.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Handlers struct {
2929
Stress func(cmd *cobra.Command, args []string) error
3030
About func(cmd *cobra.Command, args []string) error
3131
Import func(cmd *cobra.Command, args []string) error
32+
Send func(cmd *cobra.Command, args []string) error
3233
}
3334

3435
// BuildRoot builds the root command tree with optional handlers.
@@ -164,6 +165,19 @@ var cmdManifest = []CommandSpec{
164165
Short: "Show comprehensive yapi developer guide",
165166
Long: "Display a comprehensive developer guide for working with yapi. Includes syntax, examples, best practices, and project organization patterns.",
166167
},
168+
{
169+
Use: "send <url> [body]",
170+
Short: "Send a quick request without a config file",
171+
Long: "Send a one-off HTTP or TCP request directly from the command line.\nThe transport is auto-detected from the URL scheme (tcp://, grpc://, or HTTP by default).\n\nExamples:\n yapi send https://httpbin.org/get\n yapi send -X POST https://httpbin.org/post '{\"hello\":\"world\"}'\n yapi send tcp://localhost:9877 '{\"type\":\"health\",\"params\":{}}'",
172+
Args: cobra.RangeArgs(1, 2),
173+
Flags: []FlagSpec{
174+
{Name: "method", Shorthand: "X", Type: "string", Default: "", Usage: "HTTP method (default: GET, or POST if body is provided)"},
175+
{Name: "header", Shorthand: "H", Type: "stringSlice", Default: nil, Usage: "Custom headers (e.g. -H 'Content-Type: application/json')"},
176+
{Name: "verbose", Shorthand: "v", Type: "bool", Default: false, Usage: "Show verbose output (request details, timing, headers)"},
177+
{Name: "json", Type: "bool", Default: false, Usage: "Output result as JSON with full metadata"},
178+
{Name: "jq", Type: "string", Default: "", Usage: "JQ filter to apply to the response"},
179+
},
180+
},
167181
{
168182
Use: "import [file]",
169183
Short: "Import an external collection (Postman) to yapi format",
@@ -215,6 +229,8 @@ func getHandler(h *Handlers, use string) func(*cobra.Command, []string) error {
215229
return h.Stress
216230
case "about":
217231
return h.About
232+
case "send":
233+
return h.Send
218234
case "import":
219235
return h.Import
220236
default:

examples/send/sendexample1.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
# sendexample1.sh - HTTP examples using yapi send
3+
# These mirror the equivalent .yapi.yml config files in examples/http/
4+
5+
set -euo pipefail
6+
7+
echo "=== Simple GET (mirrors http/simple-get.yapi.yml) ==="
8+
yapi send https://jsonplaceholder.typicode.com/posts/1
9+
10+
echo ""
11+
echo "=== POST JSON (mirrors http/post-json.yapi.yml) ==="
12+
yapi send https://httpbin.org/post '{"title":"Hello from yapi"}' --jq '.json'
13+
14+
echo ""
15+
echo "=== GET with custom headers (mirrors http/custom-headers.yapi.yml) ==="
16+
yapi send https://httpbin.org/headers \
17+
-H 'X-Custom-Header: my-custom-value' \
18+
-H 'Accept: application/json'
19+
20+
echo ""
21+
echo "=== Explicit method with -X ==="
22+
yapi send -X PUT https://httpbin.org/put '{"updated":true}'
23+
24+
echo ""
25+
echo "=== GET with jq filter ==="
26+
yapi send https://jsonplaceholder.typicode.com/posts/1 --jq '.title'

examples/send/sendexample2.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
# sendexample2.sh - TCP and advanced examples using yapi send
3+
# These mirror the equivalent .yapi.yml config files in examples/tcp/
4+
5+
set -euo pipefail
6+
7+
echo "=== TCP echo (mirrors tcp/echo-server.yapi.yml) ==="
8+
yapi send 'tcp://tcpbin.com:4242' 'Hello from yapi!'
9+
10+
echo ""
11+
echo "=== JSON output mode ==="
12+
yapi send https://jsonplaceholder.typicode.com/posts/1 --json
13+
14+
echo ""
15+
echo "=== Verbose mode ==="
16+
yapi send -v https://httpbin.org/get
17+
18+
echo ""
19+
echo "=== POST with explicit content type ==="
20+
yapi send -X POST https://httpbin.org/post '{"id":42}' \
21+
-H 'Content-Type: application/json' \
22+
-H 'Authorization: Bearer test-token'

0 commit comments

Comments
 (0)