Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: 2.22.0 #304

Merged
merged 10 commits into from
Feb 20, 2025
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.21.0"
".": "2.22.0"
}
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 2.22.0 (2025-02-14)

Full Changelog: [v2.21.0...v2.22.0](https://github.com/Modern-Treasury/modern-treasury-go/compare/v2.21.0...v2.22.0)

### Features

* **client:** send `X-Stainless-Timeout` header ([#309](https://github.com/Modern-Treasury/modern-treasury-go/issues/309)) ([f17d1f0](https://github.com/Modern-Treasury/modern-treasury-go/commit/f17d1f0d635580e1c028e8b93ae4de9ffd536dd5))


### Bug Fixes

* **client:** don't truncate manually specified filenames ([#313](https://github.com/Modern-Treasury/modern-treasury-go/issues/313)) ([e6b36c0](https://github.com/Modern-Treasury/modern-treasury-go/commit/e6b36c0dfdbd547ab49d8c4118131bc9d746bfac))
* do not call path.Base on ContentType ([#312](https://github.com/Modern-Treasury/modern-treasury-go/issues/312)) ([4b12f61](https://github.com/Modern-Treasury/modern-treasury-go/commit/4b12f617665e5867992e4024261d7b0675f2b71e))
* fix early cancel when RequestTimeout is provided for streaming requests ([#311](https://github.com/Modern-Treasury/modern-treasury-go/issues/311)) ([3b6659a](https://github.com/Modern-Treasury/modern-treasury-go/commit/3b6659a7e7e1ce3faa6d44d68c404b17a0dafe4a))
* fix unicode encoding for json ([#306](https://github.com/Modern-Treasury/modern-treasury-go/issues/306)) ([c839501](https://github.com/Modern-Treasury/modern-treasury-go/commit/c83950183e03f6875b5d9dc44723e7518939116e))
* mark nullable property as nullable ([#305](https://github.com/Modern-Treasury/modern-treasury-go/issues/305)) ([7ca7290](https://github.com/Modern-Treasury/modern-treasury-go/commit/7ca7290d7403d4a98585b714e6d5b6264c811404))


### Chores

* add UnionUnmarshaler for responses that are interfaces ([#310](https://github.com/Modern-Treasury/modern-treasury-go/issues/310)) ([be8c5fc](https://github.com/Modern-Treasury/modern-treasury-go/commit/be8c5fc638f37dcbd36dfe2876f1faaddcdf5d0b))
* refactor client tests ([#303](https://github.com/Modern-Treasury/modern-treasury-go/issues/303)) ([1979afa](https://github.com/Modern-Treasury/modern-treasury-go/commit/1979afacd0aa200e7b8f0420fb7ace9e29a2a258))


### Documentation

* document raw responses ([#307](https://github.com/Modern-Treasury/modern-treasury-go/issues/307)) ([3fc66ac](https://github.com/Modern-Treasury/modern-treasury-go/commit/3fc66aca9b24f7cca87e8f850e80d249f165cb21))

## 2.21.0 (2025-01-22)

Full Changelog: [v2.20.5...v2.21.0](https://github.com/Modern-Treasury/modern-treasury-go/compare/v2.20.5...v2.21.0)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/Modern-Treasury/modern-treasury-go/v2@v2.21.0'
go get -u 'github.com/Modern-Treasury/modern-treasury-go/v2@v2.22.0'
```

<!-- x-release-please-end -->
2 changes: 1 addition & 1 deletion bulkresult.go
Original file line number Diff line number Diff line change
@@ -205,7 +205,7 @@ type BulkResultEntity struct {
// transaction. A `credit` moves money from your account to someone else's. A
// `debit` pulls money from someone else's account to your own. Note that wire,
// rtp, and check payments will always be `credit`.
Direction string `json:"direction"`
Direction string `json:"direction,nullable"`
DiscardedAt time.Time `json:"discarded_at,nullable" format:"date-time"`
// The timestamp (ISO8601 format) at which the ledger transaction happened for
// reporting purposes.
42 changes: 21 additions & 21 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -62,11 +62,11 @@ func TestRetryAfter(t *testing.T) {
},
}),
)
res, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("Expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("Expected there to be a cancel error")
}

attempts := len(retryCountHeaders)
@@ -98,11 +98,11 @@ func TestDeleteRetryCountHeader(t *testing.T) {
}),
option.WithHeaderDel("X-Stainless-Retry-Count"),
)
res, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("Expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("Expected there to be a cancel error")
}

expectedRetryCountHeaders := []string{"", "", ""}
@@ -129,11 +129,11 @@ func TestOverwriteRetryCountHeader(t *testing.T) {
}),
option.WithHeader("X-Stainless-Retry-Count", "42"),
)
res, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("Expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("Expected there to be a cancel error")
}

expectedRetryCountHeaders := []string{"42", "42", "42"}
@@ -159,11 +159,11 @@ func TestRetryAfterMs(t *testing.T) {
},
}),
)
res, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(context.Background(), moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("Expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("Expected there to be a cancel error")
}
if want := 3; attempts != want {
t.Errorf("Expected %d attempts, got %d", want, attempts)
@@ -183,11 +183,11 @@ func TestContextCancel(t *testing.T) {
)
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
res, err := client.Counterparties.New(cancelCtx, moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(cancelCtx, moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("Expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("Expected there to be a cancel error")
}
}

@@ -204,11 +204,11 @@ func TestContextCancelDelay(t *testing.T) {
)
cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
res, err := client.Counterparties.New(cancelCtx, moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(cancelCtx, moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("expected there to be a cancel error and for the response to be nil")
if err == nil {
t.Error("expected there to be a cancel error")
}
}

@@ -231,11 +231,11 @@ func TestContextDeadline(t *testing.T) {
},
}),
)
res, err := client.Counterparties.New(deadlineCtx, moderntreasury.CounterpartyNewParams{
_, err := client.Counterparties.New(deadlineCtx, moderntreasury.CounterpartyNewParams{
Name: moderntreasury.F("my first counterparty"),
})
if err == nil || res != nil {
t.Error("expected there to be a deadline error and for the response to be nil")
if err == nil {
t.Error("expected there to be a deadline error")
}
close(testDone)
}()
2 changes: 1 addition & 1 deletion field.go
Original file line number Diff line number Diff line change
@@ -46,5 +46,5 @@ type file struct {
contentType string
}

func (f *file) Name() string { return f.name }
func (f *file) ContentType() string { return f.contentType }
func (f *file) Filename() string { return f.name }
6 changes: 4 additions & 2 deletions internal/apiform/encoder.go
Original file line number Diff line number Diff line change
@@ -315,11 +315,13 @@ func (e *encoder) newReaderTypeEncoder() encoderFunc {
reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
filename := "anonymous_file"
contentType := "application/octet-stream"
if named, ok := reader.(interface{ Name() string }); ok {
if named, ok := reader.(interface{ Filename() string }); ok {
filename = named.Filename()
} else if named, ok := reader.(interface{ Name() string }); ok {
filename = path.Base(named.Name())
}
if typed, ok := reader.(interface{ ContentType() string }); ok {
contentType = path.Base(typed.ContentType())
contentType = typed.ContentType()
}

// Below is taken almost 1-for-1 from [multipart.CreateFormFile]
2 changes: 1 addition & 1 deletion internal/apijson/encoder.go
Original file line number Diff line number Diff line change
@@ -143,7 +143,7 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
// code more and this current code shouldn't cause any issues
case reflect.String:
return func(v reflect.Value) ([]byte, error) {
return []byte(fmt.Sprintf("%q", v.String())), nil
return json.Marshal(v.Interface())
}
case reflect.Bool:
return func(v reflect.Value) ([]byte, error) {
10 changes: 10 additions & 0 deletions internal/apijson/registry.go
Original file line number Diff line number Diff line change
@@ -29,3 +29,13 @@ func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVari
unionVariants[variant.Type] = typ
}
}

// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an
// UnmarshalJSON function on the interface itself.
type UnionUnmarshaler[T any] struct {
Value T
}

func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error {
return UnmarshalRoot(buf, &c.Value)
}
72 changes: 65 additions & 7 deletions internal/requestconfig/requestconfig.go
Original file line number Diff line number Diff line change
@@ -142,6 +142,7 @@ func NewRequestConfig(ctx context.Context, method string, u string, body interfa
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Stainless-Retry-Count", "0")
req.Header.Set("X-Stainless-Timeout", "0")
for k, v := range getDefaultHeaders() {
req.Header.Add(k, v)
}
@@ -161,6 +162,18 @@ func NewRequestConfig(ctx context.Context, method string, u string, body interfa
if err != nil {
return nil, err
}

// This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only
// apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified
// by the user and we should respect that.
if req.Header.Get("X-Stainless-Timeout") == "0" {
if cfg.RequestTimeout == time.Duration(0) {
req.Header.Del("X-Stainless-Timeout")
} else {
req.Header.Set("X-Stainless-Timeout", strconv.Itoa(int(cfg.RequestTimeout.Seconds())))
}
}

return &cfg, nil
}

@@ -285,6 +298,41 @@ func parseRetryAfterHeader(resp *http.Response) (time.Duration, bool) {
return 0, false
}

// isBeforeContextDeadline reports whether the non-zero Time t is
// before ctx's deadline. If ctx does not have a deadline, it
// always reports true (the deadline is considered infinite).
func isBeforeContextDeadline(t time.Time, ctx context.Context) bool {
d, ok := ctx.Deadline()
if !ok {
return true
}
return t.Before(d)
}

// bodyWithTimeout is an io.ReadCloser which can observe a context's cancel func
// to handle timeouts etc. It wraps an existing io.ReadCloser.
type bodyWithTimeout struct {
stop func() // stops the time.Timer waiting to cancel the request
rc io.ReadCloser
}

func (b *bodyWithTimeout) Read(p []byte) (n int, err error) {
n, err = b.rc.Read(p)
if err == nil {
return n, nil
}
if err == io.EOF {
return n, err
}
return n, err
}

func (b *bodyWithTimeout) Close() error {
err := b.rc.Close()
b.stop()
return err
}

func retryDelay(res *http.Response, retryCount int) time.Duration {
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
// just do what it says.
@@ -346,12 +394,17 @@ func (cfg *RequestConfig) Execute() (err error) {
shouldSendRetryCount := cfg.Request.Header.Get("X-Stainless-Retry-Count") == "0"

var res *http.Response
var cancel context.CancelFunc
for retryCount := 0; retryCount <= cfg.MaxRetries; retryCount += 1 {
ctx := cfg.Request.Context()
if cfg.RequestTimeout != time.Duration(0) {
var cancel context.CancelFunc
if cfg.RequestTimeout != time.Duration(0) && isBeforeContextDeadline(time.Now().Add(cfg.RequestTimeout), ctx) {
ctx, cancel = context.WithTimeout(ctx, cfg.RequestTimeout)
defer cancel()
defer func() {
// The cancel function is nil if it was handed off to be handled in a different scope.
if cancel != nil {
cancel()
}
}()
}

req := cfg.Request.Clone(ctx)
@@ -419,10 +472,15 @@ func (cfg *RequestConfig) Execute() (err error) {
return &aerr
}

if cfg.ResponseBodyInto == nil {
return nil
}
if _, ok := cfg.ResponseBodyInto.(**http.Response); ok {
_, intoCustomResponseBody := cfg.ResponseBodyInto.(**http.Response)
if cfg.ResponseBodyInto == nil || intoCustomResponseBody {
// We aren't reading the response body in this scope, but whoever is will need the
// cancel func from the context to observe request timeouts.
// Put the cancel function in the response body so it can be handled elsewhere.
if cancel != nil {
res.Body = &bodyWithTimeout{rc: res.Body, stop: cancel}
cancel = nil
}
return nil
}

2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@

package internal

const PackageVersion = "2.21.0" // x-release-please-version
const PackageVersion = "2.22.0" // x-release-please-version
2 changes: 1 addition & 1 deletion paymentorder.go
Original file line number Diff line number Diff line change
@@ -652,7 +652,7 @@ type PaymentOrderUltimateOriginatingAccount struct {
// This field can have the runtime type of [map[string]string].
Metadata interface{} `json:"metadata,required"`
// The name of the virtual account.
Name string `json:"name,required"`
Name string `json:"name,required,nullable"`
Object string `json:"object,required"`
// This field can have the runtime type of [[]RoutingDetail].
RoutingDetails interface{} `json:"routing_details,required"`