Skip to content

Commit

Permalink
Merge pull request #3 from omc/feat/spaces-client
Browse files Browse the repository at this point in the history
Feat/spaces client
  • Loading branch information
momer authored May 2, 2024
2 parents fc37fed + 0864672 commit c96e91c
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 46 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ repos:
language: golang
require_serial: true
pass_filenames: false
files: ^go\.(mod|sum)$
137 changes: 95 additions & 42 deletions bonsai/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ const (
HTTPContentTypeJSON string = "application/json"
)

// Magic numbers used to limit allocations, etc.
const (
defaultResponseCapacity = 8
defaultListResultSize = 100
)

// HTTP Status Response Errors.
var (
ErrHTTPStatusNotFound = errors.New("not found")
Expand Down Expand Up @@ -111,7 +117,24 @@ type listOpts struct {
Size int // Size of each page, with a max of 100
}

// Values returns the listOpts as URL values.
// newListOpts creates a new listOpts with default values per the API docs.
//
//nolint:unused // will be used for clusters endpoint
func newDefaultListOpts() listOpts {
return listOpts{
Page: 1,
Size: defaultListResultSize,
}
}

// newEmptyListOpts returns an empty list opts,
// to make it easy for readers to immediately see that there are no options
// being passed, rather than seeing a struct be initialized in-line.
func newEmptyListOpts() listOpts {
return listOpts{}
}

// values returns the listOpts as URL values.
func (l listOpts) values() url.Values {
vals := url.Values{}
if l.Page > 0 {
Expand All @@ -123,6 +146,14 @@ func (l listOpts) values() url.Values {
return vals
}

func (l listOpts) IsZero() bool {
return l.Page == 0 && l.Size == 0
}

func (l listOpts) Valid() bool {
return !l.IsZero()
}

type Application struct {
Name string
Version string
Expand Down Expand Up @@ -216,23 +247,56 @@ type PaginatedResponse struct {

type httpResponse = *http.Response
type Response struct {
httpResponse `json:"-"`

Body io.ReadCloser `json:"-"`
httpResponse `json:"-"`
BodyBuf bytes.Buffer `json:"-"`
PaginatedResponse `json:"pagination"`
}

// WithHTTPResponse assigns an *http.Response to a *Response item
// and reads its response body into the *Response.
func (r *Response) WithHTTPResponse(httpResp *http.Response) error {
var err error
r.httpResponse = httpResp

err := r.readHTTPResponseBody()
if err != nil {
return fmt.Errorf("reading response body for error extraction: %w", err)
}

return err
}

func (r *Response) MarkPaginationComplete() {
r.PaginatedResponse = PaginatedResponse{}
}

func (r *Response) readHTTPResponseBody() error {
var (
err error
)

_, err = r.BodyBuf.ReadFrom(r.Body)
if err != nil {
return fmt.Errorf("error reading response body: %w", err)
}

return nil
}

func extractRetryDelay(r *Response) (int64, error) {
var (
retryAfter int64
err error
)
// We're already blocking on this routine, so sleep inline per the header request.
if retryAfterStr := r.Header.Get(HeaderRetryAfter); retryAfterStr != "" {
retryAfter, err = strconv.ParseInt(retryAfterStr, 10, 64)
if err != nil {
return retryAfter, fmt.Errorf("error parsing retry-after response: %w", err)
}
}
return retryAfter, nil
}

// NewResponse reserves this function signature, and is
// the recommended way to instantiate a Response, as its behavior
// may change.
Expand All @@ -256,6 +320,9 @@ type Client struct {
endpoint string
token Token
userAgent string

// Clients
Space SpaceClient
}

func NewClient(options ...ClientOption) *Client {
Expand All @@ -272,6 +339,9 @@ func NewClient(options ...ClientOption) *Client {
option(client)
}

// Configure child clients
client.Space = SpaceClient{client}

return client
}

Expand Down Expand Up @@ -349,7 +419,7 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request, reqBuf *bytes
req.Body = io.NopCloser(reqBuf)
}

// Context cancelled, timed-out, burst issue, or other rate limit issue;
// Context canceled, timed-out, burst issue, or other rate limit issue;
// let the callers handle it.
if err := c.rateLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("failed while awaiting execution per rate-limit: %w", err)
Expand All @@ -359,6 +429,7 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request, reqBuf *bytes
if err != nil {
return nil, fmt.Errorf("http request failed: %w", err)
}
defer func() { err = IoClose(httpResp.Body, err) }()

if httpResp == nil {
return nil, errors.New("received nil http.Response")
Expand All @@ -368,15 +439,6 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request, reqBuf *bytes
if err != nil {
return resp, errors.New("creating new Response")
}
defer func() { err = IoClose(httpResp.Body, err) }()

// A place to store the response body bytes
bodyBuf := new(bytes.Buffer)

_, err = bodyBuf.ReadFrom(httpResp.Body)
if err != nil {
return resp, fmt.Errorf("error reading response body: %w", err)
}

err = resp.WithHTTPResponse(httpResp)
if err != nil {
Expand All @@ -385,62 +447,53 @@ func (c *Client) doRequest(ctx context.Context, req *http.Request, reqBuf *bytes

// Extract the pagination details
if httpResp.Header.Get("Content-Type") == HTTPContentTypeJSON {
err = json.Unmarshal(bodyBuf.Bytes(), &resp)
err = json.Unmarshal(resp.BodyBuf.Bytes(), &resp)
if err != nil {
return resp, fmt.Errorf("error unmarshaling response body: %w", err)
return resp, fmt.Errorf("error unmarshaling response body for pagination: %w", err)
}
}

if resp.StatusCode >= http.StatusBadRequest {
respErr := ResponseError{}
if err = json.Unmarshal(bodyBuf.Bytes(), &respErr); err != nil {
return resp, fmt.Errorf("error unmarshalling error response: %w", err)
if err = json.Unmarshal(resp.BodyBuf.Bytes(), &respErr); err != nil {
return resp, fmt.Errorf("unmarshalling error response: %w", err)
}
return resp, respErr
}

return resp, err
}

func extractRetryDelay(resp *Response) (int64, error) {
var (
retryAfter int64
err error
)
// We're already blocking on this routine, so sleep inline per the header request.
if retryAfterStr := resp.Header.Get(HeaderRetryAfter); retryAfterStr != "" {
retryAfter, err = strconv.ParseInt(retryAfterStr, 10, 64)
if err != nil {
return retryAfter, fmt.Errorf("error parsing retry-after response: %w", err)
}
}
return retryAfter, nil
}

// all loops through the next page pagination results until empty
// it allows the caller to pass a func (typically a closure) to collect
// results.
func (c *Client) all(ctx context.Context, f func(int) (*Response, error)) error {
var (
page = 1
)
func (c *Client) all(ctx context.Context, opt listOpts, f func(opts listOpts) (*Response, error)) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
resp, err := f(page)
resp, err := f(opt)
if err != nil {
return err
}

// The caller is responsible for determining whether or not we've exhausted
// The caller is responsible for determining whether we've exhausted
// retries.
if reflect.ValueOf(resp.PaginatedResponse).IsZero() || resp.PageNumber <= 0 {
return nil
}
// We should be fine with a straight increment, but let's play it safe
page = resp.PageNumber + 1

// If the response contains a page number, provide the next call with an
// incremented page number, and the response page size.
//
// Again, the caller must determine whether the total number of results have been delivered.
if resp.PageNumber > 0 {
opt = listOpts{
Page: resp.PageNumber + 1,
Size: resp.PageSize,
}
}
}
}
}
11 changes: 7 additions & 4 deletions bonsai/client_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,15 @@ func (s *ClientImplTestSuite) TestClientAll() {
// The caller must track results against expected count
// A reminder to the reader: this is the caller.
var resultCount = 0
err := s.client.all(context.Background(), func(page int) (*Response, error) {
s.Equalf(expectedPage, page, "expected page number (%d) matches actual (%d)", expectedPage, page)
err := s.client.all(context.Background(), newEmptyListOpts(), func(opt listOpts) (*Response, error) {
reqPath := "/"

path := fmt.Sprintf("/?page=%d&size=1", page)
if opt.Valid() {
s.Equalf(expectedPage, opt.Page, "expected page number (%d) matches actual (%d)", expectedPage, opt.Page)
reqPath = fmt.Sprintf("%s?page=%d&size=1", reqPath, opt.Page)
}

req, err := s.client.NewRequest(ctx, "GET", path, nil)
req, err := s.client.NewRequest(ctx, "GET", reqPath, nil)
s.NoError(err, "new request for path")

resp, err := s.client.Do(context.Background(), req)
Expand Down
Loading

0 comments on commit c96e91c

Please sign in to comment.