Skip to content

Commit 838c2f2

Browse files
stainless-app[bot]yjp20
authored andcommitted
feat: fix edge cases for sending request data and add YAML support
1 parent cf2cfa7 commit 838c2f2

File tree

22 files changed

+1677
-338
lines changed

22 files changed

+1677
-338
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ stl [resource] [command] [flags]
4545

4646
```sh
4747
stl builds create \
48+
--revision main \
4849
<<JSON
4950
{
50-
"project": "stainless",
51-
"revision": "main"
51+
"project": "stainless"
5252
}
5353
JSON
5454
```

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ require (
1010
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
1111
github.com/charmbracelet/x/ansi v0.8.0
1212
github.com/charmbracelet/x/term v0.2.1
13+
github.com/goccy/go-yaml v1.18.0
1314
github.com/itchyny/json2yaml v0.1.4
1415
github.com/logrusorgru/aurora/v4 v4.0.0
1516
github.com/muesli/reflow v0.3.0
1617
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1718
github.com/stainless-api/stainless-api-go v0.27.0
1819
github.com/tidwall/gjson v1.18.0
1920
github.com/tidwall/pretty v1.2.1
20-
github.com/tidwall/sjson v1.2.5
21-
github.com/urfave/cli-docs/v3 v3.1.0
22-
github.com/urfave/cli/v3 v3.4.1
21+
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
22+
github.com/urfave/cli/v3 v3.3.2
2323
golang.org/x/term v0.37.0
2424
golang.org/x/text v0.24.0
2525
)
@@ -51,6 +51,7 @@ require (
5151
github.com/rivo/uniseg v0.4.7 // indirect
5252
github.com/russross/blackfriday/v2 v2.1.0 // indirect
5353
github.com/tidwall/match v1.1.1 // indirect
54+
github.com/tidwall/sjson v1.2.5 // indirect
5455
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
5556
github.com/yuin/goldmark v1.7.8 // indirect
5657
github.com/yuin/goldmark-emoji v1.0.5 // indirect

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
6060
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
6161
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
6262
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
63+
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
64+
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
6365
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
6466
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
6567
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -113,10 +115,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
113115
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
114116
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
115117
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
116-
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
117-
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
118-
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
119-
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
118+
github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
119+
github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
120+
github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
121+
github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
120122
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
121123
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
122124
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=

internal/apiform/encoder.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package apiform
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"mime/multipart"
7+
"net/textproto"
8+
"path"
9+
"reflect"
10+
"sort"
11+
"strconv"
12+
"strings"
13+
)
14+
15+
// Marshal encodes a value as multipart form data using default settings
16+
func Marshal(value any, writer *multipart.Writer) error {
17+
e := &encoder{
18+
format: FormatRepeat,
19+
}
20+
return e.marshal(value, writer)
21+
}
22+
23+
// MarshalWithSettings encodes a value with custom array format
24+
func MarshalWithSettings(value any, writer *multipart.Writer, arrayFormat FormFormat) error {
25+
e := &encoder{
26+
format: arrayFormat,
27+
}
28+
return e.marshal(value, writer)
29+
}
30+
31+
type encoder struct {
32+
format FormFormat
33+
}
34+
35+
func (e *encoder) marshal(value any, writer *multipart.Writer) error {
36+
val := reflect.ValueOf(value)
37+
if !val.IsValid() {
38+
return nil
39+
}
40+
return e.encodeValue("", val, writer)
41+
}
42+
43+
func (e *encoder) encodeValue(key string, val reflect.Value, writer *multipart.Writer) error {
44+
if !val.IsValid() {
45+
return writer.WriteField(key, "")
46+
}
47+
48+
t := val.Type()
49+
50+
if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
51+
return e.encodeReader(key, val, writer)
52+
}
53+
54+
switch t.Kind() {
55+
case reflect.Pointer:
56+
if val.IsNil() || !val.IsValid() {
57+
return writer.WriteField(key, "")
58+
}
59+
return e.encodeValue(key, val.Elem(), writer)
60+
61+
case reflect.Slice, reflect.Array:
62+
return e.encodeArray(key, val, writer)
63+
64+
case reflect.Map:
65+
return e.encodeMap(key, val, writer)
66+
67+
case reflect.Interface:
68+
if val.IsNil() {
69+
return writer.WriteField(key, "")
70+
}
71+
return e.encodeValue(key, val.Elem(), writer)
72+
73+
case reflect.String:
74+
return writer.WriteField(key, val.String())
75+
76+
case reflect.Bool:
77+
if val.Bool() {
78+
return writer.WriteField(key, "true")
79+
}
80+
return writer.WriteField(key, "false")
81+
82+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
83+
return writer.WriteField(key, strconv.FormatInt(val.Int(), 10))
84+
85+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
86+
return writer.WriteField(key, strconv.FormatUint(val.Uint(), 10))
87+
88+
case reflect.Float32:
89+
return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 32))
90+
91+
case reflect.Float64:
92+
return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 64))
93+
94+
default:
95+
return fmt.Errorf("unknown type: %s", t.String())
96+
}
97+
}
98+
99+
func (e *encoder) encodeArray(key string, val reflect.Value, writer *multipart.Writer) error {
100+
if e.format == FormatComma {
101+
var values []string
102+
for i := 0; i < val.Len(); i++ {
103+
item := val.Index(i)
104+
var strValue string
105+
switch item.Kind() {
106+
case reflect.String:
107+
strValue = item.String()
108+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
109+
strValue = strconv.FormatInt(item.Int(), 10)
110+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
111+
strValue = strconv.FormatUint(item.Uint(), 10)
112+
case reflect.Float32, reflect.Float64:
113+
strValue = strconv.FormatFloat(item.Float(), 'f', -1, 64)
114+
case reflect.Bool:
115+
strValue = strconv.FormatBool(item.Bool())
116+
default:
117+
return fmt.Errorf("comma format not supported for complex array elements")
118+
}
119+
values = append(values, strValue)
120+
}
121+
return writer.WriteField(key, strings.Join(values, ","))
122+
}
123+
124+
for i := 0; i < val.Len(); i++ {
125+
var formattedKey string
126+
switch e.format {
127+
case FormatRepeat:
128+
formattedKey = key
129+
case FormatBrackets:
130+
formattedKey = key + "[]"
131+
case FormatIndicesDots:
132+
if key == "" {
133+
formattedKey = strconv.Itoa(i)
134+
} else {
135+
formattedKey = key + "." + strconv.Itoa(i)
136+
}
137+
case FormatIndicesBrackets:
138+
if key == "" {
139+
formattedKey = strconv.Itoa(i)
140+
} else {
141+
formattedKey = key + "[" + strconv.Itoa(i) + "]"
142+
}
143+
default:
144+
return fmt.Errorf("apiform: unsupported array format")
145+
}
146+
147+
if err := e.encodeValue(formattedKey, val.Index(i), writer); err != nil {
148+
return err
149+
}
150+
}
151+
return nil
152+
}
153+
154+
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
155+
156+
func escapeQuotes(s string) string {
157+
return quoteEscaper.Replace(s)
158+
}
159+
160+
func (e *encoder) encodeReader(key string, val reflect.Value, writer *multipart.Writer) error {
161+
reader, ok := val.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
162+
if !ok {
163+
return nil
164+
}
165+
166+
// Set defaults
167+
filename := "anonymous_file"
168+
contentType := "application/octet-stream"
169+
170+
// Get filename if available
171+
if named, ok := reader.(interface{ Filename() string }); ok {
172+
filename = named.Filename()
173+
} else if named, ok := reader.(interface{ Name() string }); ok {
174+
filename = path.Base(named.Name())
175+
}
176+
177+
// Get content type if available
178+
if typed, ok := reader.(interface{ ContentType() string }); ok {
179+
contentType = typed.ContentType()
180+
}
181+
182+
h := make(textproto.MIMEHeader)
183+
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
184+
escapeQuotes(key), escapeQuotes(filename)))
185+
h.Set("Content-Type", contentType)
186+
187+
filewriter, err := writer.CreatePart(h)
188+
if err != nil {
189+
return err
190+
}
191+
_, err = io.Copy(filewriter, reader)
192+
return err
193+
}
194+
195+
func (e *encoder) encodeMap(key string, val reflect.Value, writer *multipart.Writer) error {
196+
type mapPair struct {
197+
key string
198+
value reflect.Value
199+
}
200+
201+
if key != "" {
202+
key = key + "."
203+
}
204+
205+
// Collect and sort map entries for deterministic output
206+
pairs := []mapPair{}
207+
iter := val.MapRange()
208+
for iter.Next() {
209+
if iter.Key().Type().Kind() != reflect.String {
210+
return fmt.Errorf("cannot encode a map with a non string key")
211+
}
212+
pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
213+
}
214+
215+
sort.Slice(pairs, func(i, j int) bool {
216+
return pairs[i].key < pairs[j].key
217+
})
218+
219+
// Process sorted pairs
220+
for _, p := range pairs {
221+
if err := e.encodeValue(key+p.key, p.value, writer); err != nil {
222+
return err
223+
}
224+
}
225+
226+
return nil
227+
}

internal/apiform/form.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package apiform
2+
3+
type Marshaler interface {
4+
MarshalMultipart() ([]byte, string, error)
5+
}
6+
7+
type FormFormat int
8+
9+
const (
10+
// FormatRepeat represents arrays as repeated keys with the same value
11+
FormatRepeat FormFormat = iota
12+
// Comma-separated values 1,2,3
13+
FormatComma
14+
// FormatBrackets uses the key[] notation for arrays
15+
FormatBrackets
16+
// FormatIndicesDots uses key.0, key.1, etc. notation
17+
FormatIndicesDots
18+
// FormatIndicesBrackets uses key[0], key[1], etc. notation
19+
FormatIndicesBrackets
20+
)

0 commit comments

Comments
 (0)