Skip to content

Commit 94bf74c

Browse files
stainless-app[bot]batuhan
authored andcommitted
feat: support passing required body params through pipes
1 parent 38329b7 commit 94bf74c

14 files changed

Lines changed: 558 additions & 342 deletions

internal/binaryparam/binary_param.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func FileOrStdin(stdin io.ReadCloser, path string) (io.ReadCloser, bool, error)
1717
// When the special glyph "-" is used, read from stdin. Although probably less necessary, also support
1818
// special Unix files that refer to stdin.
1919
switch path {
20-
case stdinGlyph, "/dev/fd/0", "/dev/stdin":
20+
case "", stdinGlyph, "/dev/fd/0", "/dev/stdin":
2121
return stdin, true, nil
2222
}
2323

internal/binaryparam/binary_param_test.go

Lines changed: 28 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,57 +28,32 @@ func TestFileOrStdin(t *testing.T) {
2828
require.False(t, stdinInUse)
2929
})
3030

31-
t.Run("WithStdinGlyph", func(t *testing.T) {
32-
tempFile := t.TempDir() + "/test_file.txt"
33-
require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600))
34-
35-
stubStdin, err := os.Open(tempFile)
36-
require.NoError(t, err)
37-
t.Cleanup(func() { require.NoError(t, stubStdin.Close()) })
38-
39-
readCloser, stdinInUse, err := FileOrStdin(stubStdin, "-")
40-
require.NoError(t, err)
41-
42-
actualContents, err := io.ReadAll(readCloser)
43-
require.NoError(t, err)
44-
require.Equal(t, expectedContents, string(actualContents))
45-
46-
require.True(t, stdinInUse)
47-
})
48-
49-
t.Run("WithDevFD0File", func(t *testing.T) {
50-
tempFile := t.TempDir() + "/dev_fd_0"
51-
require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600))
52-
53-
stubStdin, err := os.Open(tempFile)
54-
require.NoError(t, err)
55-
t.Cleanup(func() { require.NoError(t, stubStdin.Close()) })
56-
57-
readCloser, stdinInUse, err := FileOrStdin(stubStdin, "/dev/fd/0")
58-
require.NoError(t, err)
59-
60-
actualContents, err := io.ReadAll(readCloser)
61-
require.NoError(t, err)
62-
require.Equal(t, expectedContents, string(actualContents))
63-
64-
require.True(t, stdinInUse)
65-
})
66-
67-
t.Run("WithDevStdinFile", func(t *testing.T) {
68-
tempFile := t.TempDir() + "/dev_stdin"
69-
require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600))
70-
71-
stubStdin, err := os.Open(tempFile)
72-
require.NoError(t, err)
73-
t.Cleanup(func() { require.NoError(t, stubStdin.Close()) })
74-
75-
readCloser, stdinInUse, err := FileOrStdin(stubStdin, "/dev/stdin")
76-
require.NoError(t, err)
77-
78-
actualContents, err := io.ReadAll(readCloser)
79-
require.NoError(t, err)
80-
require.Equal(t, expectedContents, string(actualContents))
81-
82-
require.True(t, stdinInUse)
83-
})
31+
stdinTests := []struct {
32+
testName string
33+
path string
34+
}{
35+
{"TestEmptyString", ""},
36+
{"TestDash", "-"},
37+
{"TestDevStdin", "/dev/stdin"},
38+
{"TestDevFD0", "/dev/fd/0"},
39+
}
40+
for _, test := range stdinTests {
41+
t.Run(test.testName, func(t *testing.T) {
42+
tempFile := t.TempDir() + "/test_file.txt"
43+
require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600))
44+
45+
stubStdin, err := os.Open(tempFile)
46+
require.NoError(t, err)
47+
t.Cleanup(func() { require.NoError(t, stubStdin.Close()) })
48+
49+
readCloser, stdinInUse, err := FileOrStdin(stubStdin, test.path)
50+
require.NoError(t, err)
51+
52+
actualContents, err := io.ReadAll(readCloser)
53+
require.NoError(t, err)
54+
require.Equal(t, expectedContents, string(actualContents))
55+
56+
require.True(t, stdinInUse)
57+
})
58+
}
8459
}

internal/mocktest/mocktest.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mocktest
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"net"
@@ -54,8 +55,14 @@ func restoreNetwork(origClient, origDefault http.RoundTripper) {
5455
}
5556

5657
// TestRunMockTestWithFlags runs a test against a mock server with the provided
57-
// CLI flags and ensures it succeeds
58-
func TestRunMockTestWithFlags(t *testing.T, flags ...string) {
58+
// CLI args and ensures it succeeds
59+
func TestRunMockTestWithFlags(t *testing.T, args ...string) {
60+
TestRunMockTestWithPipeAndFlags(t, nil, args...)
61+
}
62+
63+
// TestRunMockTestWithPipeAndFlags runs a test against a mock server with the provided
64+
// data piped over stdin and CLI args and ensures it succeeds
65+
func TestRunMockTestWithPipeAndFlags(t *testing.T, pipeData []byte, args ...string) {
5966
origClient, origDefault := blockNetworkExceptMockServer()
6067
defer restoreNetwork(origClient, origDefault)
6168

@@ -71,14 +78,14 @@ func TestRunMockTestWithFlags(t *testing.T, flags ...string) {
7178
_, filename, _, ok := runtime.Caller(0)
7279
require.True(t, ok, "Could not get current file path")
7380
dirPath := filepath.Dir(filename)
74-
project := filepath.Join(dirPath, "..", "..", "cmd", "...")
81+
project := filepath.Join(dirPath, "..", "..", "cmd", "beeper-desktop-cli")
7582

76-
args := []string{"run", project, "--base-url", mockServerURL.String()}
77-
args = append(args, flags...)
83+
args = append([]string{"run", project, "--base-url", mockServerURL.String()}, args...)
7884

79-
t.Logf("Testing command: beeper-desktop-cli %s", strings.Join(args[4:], " "))
85+
t.Logf("Testing command: go run ./cmd/beeper-desktop-cli %s", strings.Join(args[2:], " "))
8086

8187
cliCmd := exec.Command("go", args...)
88+
cliCmd.Stdin = bytes.NewReader(pipeData)
8289

8390
// Pipe the CLI tool's output into `head` so it doesn't hang when simulating
8491
// paginated or streamed endpoints. 100 lines of output should be enough to

internal/requestflag/requestflag.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,40 @@ func ExtractRequestContents(cmd *cli.Command) RequestContents {
110110
return res
111111
}
112112

113+
func GetMissingRequiredFlags(cmd *cli.Command, body any) []cli.Flag {
114+
missing := []cli.Flag{}
115+
for _, flag := range cmd.Flags {
116+
if flag.IsSet() {
117+
continue
118+
}
119+
120+
if required, ok := flag.(cli.RequiredFlag); ok && required.IsRequired() {
121+
missing = append(missing, flag)
122+
continue
123+
}
124+
125+
if r, ok := flag.(RequiredFlagOrStdin); !ok || !r.IsRequiredAsFlagOrStdin() {
126+
continue
127+
}
128+
129+
if toSend, ok := flag.(InRequest); ok {
130+
if toSend.IsBodyRoot() {
131+
if body != nil {
132+
continue
133+
}
134+
} else if bodyPath := toSend.GetBodyPath(); bodyPath != "" {
135+
if bodyMap, ok := body.(map[string]any); ok {
136+
if _, found := bodyMap[bodyPath]; found {
137+
continue
138+
}
139+
}
140+
}
141+
}
142+
missing = append(missing, flag)
143+
}
144+
return missing
145+
}
146+
113147
// Implementation of the cli.Flag interface
114148
var _ cli.Flag = (*Flag[any])(nil) // Type assertion to ensure interface compliance
115149

@@ -221,6 +255,19 @@ func (f *Flag[T]) SetCategory(c string) {
221255
var _ cli.RequiredFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance
222256

223257
func (f *Flag[T]) IsRequired() bool {
258+
// Intentionally don't use `f.Required`, because request flags may be passed
259+
// over stdin as well as by flag.
260+
if f.BodyPath != "" || f.BodyRoot {
261+
return false
262+
}
263+
return f.Required
264+
}
265+
266+
type RequiredFlagOrStdin interface {
267+
IsRequiredAsFlagOrStdin() bool
268+
}
269+
270+
func (f *Flag[T]) IsRequiredAsFlagOrStdin() bool {
224271
return f.Required
225272
}
226273

pkg/cmd/account_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import (
99
)
1010

1111
func TestAccountsList(t *testing.T) {
12-
mocktest.TestRunMockTestWithFlags(
13-
t,
14-
"accounts", "list",
15-
"--access-token", "string",
16-
)
12+
t.Run("regular flags", func(t *testing.T) {
13+
mocktest.TestRunMockTestWithFlags(
14+
t, "accounts", "list",
15+
"--access-token", "string",
16+
)
17+
})
1718
}

pkg/cmd/accountcontact_test.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,26 @@ import (
99
)
1010

1111
func TestAccountsContactsList(t *testing.T) {
12-
mocktest.TestRunMockTestWithFlags(
13-
t,
14-
"accounts:contacts", "list",
15-
"--access-token", "string",
16-
"--account-id", "accountID",
17-
"--cursor", "1725489123456|c29tZUltc2dQYWdl",
18-
"--direction", "before",
19-
"--limit", "1",
20-
"--query", "x",
21-
)
12+
t.Run("regular flags", func(t *testing.T) {
13+
mocktest.TestRunMockTestWithFlags(
14+
t, "accounts:contacts", "list",
15+
"--access-token", "string",
16+
"--account-id", "accountID",
17+
"--cursor", "1725489123456|c29tZUltc2dQYWdl",
18+
"--direction", "before",
19+
"--limit", "1",
20+
"--query", "x",
21+
)
22+
})
2223
}
2324

2425
func TestAccountsContactsSearch(t *testing.T) {
25-
mocktest.TestRunMockTestWithFlags(
26-
t,
27-
"accounts:contacts", "search",
28-
"--access-token", "string",
29-
"--account-id", "accountID",
30-
"--query", "x",
31-
)
26+
t.Run("regular flags", func(t *testing.T) {
27+
mocktest.TestRunMockTestWithFlags(
28+
t, "accounts:contacts", "search",
29+
"--access-token", "string",
30+
"--account-id", "accountID",
31+
"--query", "x",
32+
)
33+
})
3234
}

pkg/cmd/asset_test.go

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,77 @@ import (
99
)
1010

1111
func TestAssetsDownload(t *testing.T) {
12-
mocktest.TestRunMockTestWithFlags(
13-
t,
14-
"assets", "download",
15-
"--access-token", "string",
16-
"--url", "mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
17-
)
12+
t.Run("regular flags", func(t *testing.T) {
13+
mocktest.TestRunMockTestWithFlags(
14+
t, "assets", "download",
15+
"--access-token", "string",
16+
"--url", "mxc://example.org/Q4x9CqGz1pB3Oa6XgJ",
17+
)
18+
})
19+
20+
t.Run("piping data", func(t *testing.T) {
21+
// Test piping YAML data over stdin
22+
pipeData := []byte("url: mxc://example.org/Q4x9CqGz1pB3Oa6XgJ")
23+
mocktest.TestRunMockTestWithPipeAndFlags(
24+
t, pipeData, "assets", "download",
25+
"--access-token", "string",
26+
)
27+
})
1828
}
1929

2030
func TestAssetsServe(t *testing.T) {
21-
mocktest.TestRunMockTestWithFlags(
22-
t,
23-
"assets", "serve",
24-
"--access-token", "string",
25-
"--url", "x",
26-
)
31+
t.Run("regular flags", func(t *testing.T) {
32+
mocktest.TestRunMockTestWithFlags(
33+
t, "assets", "serve",
34+
"--access-token", "string",
35+
"--url", "x",
36+
)
37+
})
2738
}
2839

2940
func TestAssetsUpload(t *testing.T) {
30-
mocktest.TestRunMockTestWithFlags(
31-
t,
32-
"assets", "upload",
33-
"--access-token", "string",
34-
"--file", "...",
35-
"--file-name", "fileName",
36-
"--mime-type", "mimeType",
37-
)
41+
t.Run("regular flags", func(t *testing.T) {
42+
mocktest.TestRunMockTestWithFlags(
43+
t, "assets", "upload",
44+
"--access-token", "string",
45+
"--file", "...",
46+
"--file-name", "fileName",
47+
"--mime-type", "mimeType",
48+
)
49+
})
50+
51+
t.Run("piping data", func(t *testing.T) {
52+
// Test piping YAML data over stdin
53+
pipeData := []byte("" +
54+
"fileName: fileName\n" +
55+
"mimeType: mimeType\n")
56+
mocktest.TestRunMockTestWithPipeAndFlags(
57+
t, pipeData, "assets", "upload",
58+
"--access-token", "string",
59+
)
60+
})
3861
}
3962

4063
func TestAssetsUploadBase64(t *testing.T) {
41-
mocktest.TestRunMockTestWithFlags(
42-
t,
43-
"assets", "upload-base64",
44-
"--access-token", "string",
45-
"--content", "x",
46-
"--file-name", "fileName",
47-
"--mime-type", "mimeType",
48-
)
64+
t.Run("regular flags", func(t *testing.T) {
65+
mocktest.TestRunMockTestWithFlags(
66+
t, "assets", "upload-base64",
67+
"--access-token", "string",
68+
"--content", "x",
69+
"--file-name", "fileName",
70+
"--mime-type", "mimeType",
71+
)
72+
})
73+
74+
t.Run("piping data", func(t *testing.T) {
75+
// Test piping YAML data over stdin
76+
pipeData := []byte("" +
77+
"content: x\n" +
78+
"fileName: fileName\n" +
79+
"mimeType: mimeType\n")
80+
mocktest.TestRunMockTestWithPipeAndFlags(
81+
t, pipeData, "assets", "upload-base64",
82+
"--access-token", "string",
83+
)
84+
})
4985
}

0 commit comments

Comments
 (0)