Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions click/click.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ func (s *Service) Create(clickwrap *Clickwrap) *CreateOp {
}

// Do executes the operation
func (op *CreateOp) Do(ctx context.Context) (*Summary, error) {
func (op *CreateOp) Do(ctx context.Context) (*Summary, *esign.ResponseContext, error) {
var res *Summary
return res, ((*esign.Op)(op)).Do(ctx, &res)
respCtx, err := ((*esign.Op)(op)).Do(ctx, &res)
return res, respCtx, err
}

// UpdateOp updates an existing clickwrap and creates a new
Expand All @@ -73,9 +74,10 @@ func (s *Service) Update(id string, clickwrap *Clickwrap) *UpdateOp {
}

// Do executes the operation
func (op *UpdateOp) Do(ctx context.Context) (*Summary, error) {
func (op *UpdateOp) Do(ctx context.Context) (*Summary, *esign.ResponseContext, error) {
var res *Summary
return res, ((*esign.Op)(op)).Do(ctx, &res)
respCtx, err := ((*esign.Op)(op)).Do(ctx, &res)
return res, respCtx, err
}

// ListOp gets all the clickwraps for an account
Expand Down Expand Up @@ -146,9 +148,10 @@ func (op *ListOp) VersionNumber(val int32) *ListOp {
}

// Do executes the operation
func (op *ListOp) Do(ctx context.Context) (*Listing, error) {
func (op *ListOp) Do(ctx context.Context) (*Listing, *esign.ResponseContext, error) {
var res *Listing
return res, ((*esign.Op)(op)).Do(ctx, &res)
respCtx, err := ((*esign.Op)(op)).Do(ctx, &res)
return res, respCtx, err
}

// SetAgreementOp creates/update as user agreement for a client
Expand All @@ -167,9 +170,10 @@ func (s *Service) SetAgreement(clickwrapID string, agreement *UserAgreement) *Se
}

// Do executes the operation
func (op *SetAgreementOp) Do(ctx context.Context) (*UserAgreement, error) {
func (op *SetAgreementOp) Do(ctx context.Context) (*UserAgreement, *esign.ResponseContext, error) {
var res *UserAgreement
return res, ((*esign.Op)(op)).Do(ctx, &res)
respCtx, err := ((*esign.Op)(op)).Do(ctx, &res)
return res, respCtx, err
}

// GetAgreementsOp lists user agreements for a specific clickwrap
Expand Down Expand Up @@ -203,7 +207,8 @@ func (op *GetAgreementsOp) Page(val int32) *GetAgreementsOp {
}

// Do executes the operation
func (op *GetAgreementsOp) Do(ctx context.Context) (*AgreementList, error) {
func (op *GetAgreementsOp) Do(ctx context.Context) (*AgreementList, *esign.ResponseContext, error) {
var res *AgreementList
return res, ((*esign.Op)(op)).Do(ctx, &res)
respCtx, err := ((*esign.Op)(op)).Do(ctx, &res)
return res, respCtx, err
}
2 changes: 1 addition & 1 deletion click/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Example() {
if err != nil {
log.Fatal(err)
}
result, err := click.New(credential).List().Do(ctx)
result, _, err := click.New(credential).List().Do(ctx)
if err != nil {
log.Fatalf("list clickwraps error: %v", err)
}
Expand Down
88 changes: 79 additions & 9 deletions esign.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ import (
"fmt"
"io"
"mime/multipart"
"strings"

"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"
"time"
)

// Constants for the rate limit headers as described by https://developers.docusign.com/docs/esign-soap-api/esign101/security/call-limits/
const (
Header_RateLimit_Limit = "X-RateLimit-Limit"
Header_RateLimit_Remaining = "X-RateLimit-Remaining"
Header_RateLimit_Reset = "X-RateLimit-Reset"
Header_BurstLimit_Limit = "X-BurstLimit-Limit"
Header_BurstLimit_Remaining = "X-BurstLimit-Remaining"
)

// ErrNilOp used to indicate a nil operation pointer
Expand Down Expand Up @@ -220,15 +230,74 @@ func (op *Op) validate(ctx context.Context) error {
return err
}

type Ratelimit struct {
RateLimit int64
RateRemaining int64
RateReset time.Time
BurstLimit int64
BurstRemaining int64
}

func rateLimitFromHTTPResponse(resp *http.Response) *Ratelimit {
var rateLimitCount int64
var rateLimitRemainingCount int64
var rateResetEpochCount int64
var burstLimitCount int64
var burstLimitRemainingCount int64
var err error

rateLimit := resp.Header.Get(Header_RateLimit_Limit)
rateLimitCount, err = strconv.ParseInt(rateLimit, 10, 64)
if err != nil {
return nil
}

rateLimitRemaining := resp.Header.Get(Header_RateLimit_Remaining)
rateLimitRemainingCount, err = strconv.ParseInt(rateLimitRemaining, 10, 64)
if err != nil {
return nil
}

rateResetEpoch := resp.Header.Get(Header_RateLimit_Reset)
rateResetEpochCount, err = strconv.ParseInt(rateResetEpoch, 10, 64)
if err != nil {
return nil
}

burstLimit := resp.Header.Get(Header_BurstLimit_Limit)
burstLimitCount, err = strconv.ParseInt(burstLimit, 10, 64)
if err != nil {
return nil
}

burstLimitRemaining := resp.Header.Get(Header_BurstLimit_Remaining)
burstLimitRemainingCount, err = strconv.ParseInt(burstLimitRemaining, 10, 64)
if err != nil {
return nil
}

return &Ratelimit{
RateLimit: rateLimitCount,
RateRemaining: rateLimitRemainingCount,
RateReset: time.Unix(rateResetEpochCount, 0),
BurstLimit: burstLimitCount,
BurstRemaining: burstLimitRemainingCount,
}
}

type ResponseContext struct {
Ratelimit *Ratelimit
}

// Do sends a request to DocuSign. Response data is decoded into
// result. If result is a **Download, do sets the File.ReadCloser
// to the *http.Response. The developer is responsible for closing
// the Download.ReadCloser. Any non-2xx status code is returned as a
// *ResponseError.
func (op *Op) Do(ctx context.Context, result interface{}) error {
func (op *Op) Do(ctx context.Context, result interface{}) (*ResponseContext, error) {
// do nil checks and get client
if err := op.validate(ctx); err != nil {
return err
return nil, err
}

acceptHdr := op.Accept
Expand All @@ -243,18 +312,20 @@ func (op *Op) Do(ctx context.Context, result interface{}) error {
// get request
req, err := op.createOpRequest(ctx, acceptHdr)
if err != nil {
return err
return nil, err
}

res, err := op.Credential.AuthDo(ctx, req, op.Version)
if err != nil {
return err
return nil, err
}

limit := rateLimitFromHTTPResponse(res)

switch f := result.(type) {
case **Download: // return w/o closing response body
*f = &Download{res.Body, res.ContentLength, res.Header.Get("Content-Type")}
return nil
return &ResponseContext{Ratelimit: limit}, nil
case interface{}: // non-nil
// parse response and check for context cancellation.
done := make(chan error, 1) // buffered channel so go routine doesn't hang
Expand All @@ -268,8 +339,7 @@ func (op *Op) Do(ctx context.Context, result interface{}) error {
}
}
res.Body.Close()
return err

return &ResponseContext{Ratelimit: limit}, err
}

// multiPartBody sends files thru a multipart writer. Using io.Pipe
Expand Down
34 changes: 18 additions & 16 deletions esign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestOp_Do(t *testing.T) {
},
}
for i, op := range ops {
if err := op.Do(context.Background(), nil); err != nil {
if _, err := op.Do(context.Background(), nil); err != nil {
t.Errorf("Error %d: %v", i, err)
}
}
Expand All @@ -165,7 +165,7 @@ func TestOp_Do(t *testing.T) {
QueryOpts: make(url.Values),
Method: "GET",
}
err := op.Do(context.Background(), nil)
_, err := op.Do(context.Background(), nil)
if ex, ok := err.(*esign.ResponseError); !ok {
t.Fatalf("test 5 expected *ResponseError; got %#v", err)
} else if ex.Status != 400 || string(ex.Raw) != "No JSON" {
Expand All @@ -174,7 +174,7 @@ func TestOp_Do(t *testing.T) {

// check error handling for properly formateed ResponseError
op.Path = "do/test6/go"
err = op.Do(context.Background(), nil)
_, err = op.Do(context.Background(), nil)
if ex, ok := err.(*esign.ResponseError); !ok {
t.Fatalf("test 6 expected *ResponseError; got %#v", err)
} else if ex.Status != 400 || ex.ErrorCode != "A" || ex.Description != "error desc" {
Expand All @@ -189,7 +189,7 @@ func TestOp_Do(t *testing.T) {
B int64
C string
}
if err := op.Do(context.Background(), &result); err != nil {
if _, err := op.Do(context.Background(), &result); err != nil {
t.Fatalf("JSON response failed: %#v", err)
}
if result.A != "val" || result.B != 9 || result.C != "X" {
Expand Down Expand Up @@ -321,7 +321,7 @@ func TestOp_Do_FileUpload(t *testing.T) {
},
},
}
if err := op.Do(context.Background(), nil); err != nil {
if _, err := op.Do(context.Background(), nil); err != nil {
t.Fatalf("multipart test expected success; got %v", err)
}
if !checkFilesClosed(t, f1, f2) {
Expand All @@ -335,7 +335,7 @@ func TestOp_Do_FileUpload(t *testing.T) {
Transport: &ctxclient.ErrorTransport{Err: errors.New("ERROR")},
})
ctx := context.Background()
err := op.Do(ctx, nil)
_, err := op.Do(ctx, nil)
switch reterr := err.(type) {
case nil:
t.Fatalf("multipart test expected *url.Error; got success")
Expand Down Expand Up @@ -365,7 +365,8 @@ func TestOp_Do_FileUpload(t *testing.T) {
Response: testutils.MakeResponse(200, nil, nil),
})

switch err := op.Do(context.Background(), nil).(type) {
_, e := op.Do(context.Background(), nil)
switch err := e.(type) {
case *url.Error:
default:
t.Errorf("multipart read expected io error; got %v", err)
Expand Down Expand Up @@ -398,7 +399,7 @@ func TestOp_Do_FileDownload(t *testing.T) {
return res, nil
},
})
if err := op.Do(context.Background(), &file); err != nil {
if _, err := op.Do(context.Background(), &file); err != nil {
t.Fatalf("expecte esign.Download; got error %v", err)
}
if file == nil {
Expand Down Expand Up @@ -441,14 +442,14 @@ func TestOp_FilesClosed(t *testing.T) {
}
// ensure files close on nil context/invalid client/invalid credential error
var ctx context.Context
if err := op.Do(ctx, nil); err != nil && err.Error() != "nil context" {
if _, err := op.Do(ctx, nil); err != nil && err.Error() != "nil context" {
t.Errorf("expected nil context; got %v", err)
}
if !checkFilesClosed(t, f1, f2) {
t.Fatalf("multipart test nil context expected closed files")
}
ctx = context.Background()
if err := op.Do(ctx, nil); err != nil && err.Error() != "nil credential" {
if _, err := op.Do(ctx, nil); err != nil && err.Error() != "nil credential" {
t.Errorf("expected nil credential; got %v", err)
}
if !checkFilesClosed(t, f1, f2) {
Expand All @@ -459,7 +460,7 @@ func TestOp_FilesClosed(t *testing.T) {
cx, _ := getTestCredentialClientTransport()
op.Credential = cx
op.Method = "PO ST" //invalid method
if err := op.Do(ctx, nil); err != nil && err.Error() != "net/http: invalid method \"PO ST\"" {
if _, err := op.Do(ctx, nil); err != nil && err.Error() != "net/http: invalid method \"PO ST\"" {
t.Errorf("expected net/http: invalid method \"PO ST\"; got %v", err)
}
time.Sleep(time.Second)
Expand All @@ -483,7 +484,8 @@ func TestOp_Do_ContextCancel(t *testing.T) {
Response: testutils.MakeResponse(200, []byte("0123456789"), nil),
})
var result *TokenCache
switch err := op.Do(ctx, &result).(type) {
_, e := op.Do(ctx, &result)
switch err := e.(type) {
case *json.UnmarshalTypeError:
default:
t.Fatalf("expected *json.UnmarshalTypeError; got %#v", err)
Expand All @@ -504,7 +506,7 @@ func TestOp_Do_ContextCancel(t *testing.T) {
QueryOpts: make(url.Values),
Method: "POST",
}
if err := op.Do(ctx, &result); err == nil || err.Error() != "context canceled" {
if _, err := op.Do(ctx, &result); err == nil || err.Error() != "context canceled" {
t.Errorf("expected context canceled; got %v", err)
}
}
Expand Down Expand Up @@ -545,7 +547,7 @@ func TestGeneratedOps(t *testing.T) {
ctx := context.Background()
// Read throught all folders
sv := folders.New(cred)
l, err := sv.List().Do(ctx)
l, _, err := sv.List().Do(ctx)
if err != nil {
t.Errorf("List: %v", err)
return
Expand All @@ -556,14 +558,14 @@ func TestGeneratedOps(t *testing.T) {

// Read through all templates
svT := templates.New(cred)
tList, err := svT.List().Do(context.Background())
tList, _, err := svT.List().Do(context.Background())
if err != nil {
t.Errorf("Template List: %v", err)
}

for _, tmpl := range tList.EnvelopeTemplates {
t.Logf("Getting: %s", tmpl.TemplateID)
tx, err := svT.Get(tmpl.TemplateID).Include("recipients").Do(context.Background())
tx, _, err := svT.Get(tmpl.TemplateID).Include("recipients").Do(context.Background())
if err != nil {
t.Errorf("unable to open template %s: %v", tmpl.Name, err)
continue
Expand Down
Loading