clientx

package module
v0.0.0-...-a68baaa Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2024 License: MIT Imports: 22 Imported by: 1

README

Golang client for API building

The purpose of this client is to design and develop clients for any API very fast using generics for request, response models encoding/decoding with supported from the box retry, rate-limit, GZIP/Deflate decoding functionality.

Installation

NOTE: Requires at least Go 1.18 since we use generics

To get latest version use:

go get github.com/0x9ef/clientx@latest

To specify version use:

go get github.com/0x9ef/clientx@1.24.4 # version

Usage examples

Getting Started

The first thing you need to understand: it will be easy :)

The client was developed with consuming needs of modern API development. I have designed and integrated many clients for different APIs and came up with a couple useful things. I was intended to make it easy, understandable even for beginner, and include top of the most necessary functionality.

Initialize

When you are initializing client, you MUST specify base URL by clientx.WithBaseURL option.

api := clientx.NewAPI(
	clientx.WithBaseURL("https://php-noise.com"),
}
Authorizarion

There is no separate flow for authorization, but it can be done with HTTP headers. Let's talk about Bearer authorization. You have the API Access Token and you have to build HTTP Header: Authorizarion: Bearer MY_ACCESS_TOKEN, you could pass it through request options.

// GetOffer accepts offerId and request options that will be applied before request is sent.
func (api *MyAPI) GetOffer(ctx context.Context, offerId string, opts ...clientx.RequestOption) (*Offer, error) {
	return clientx.NewRequestBuilder[struct{}, Offer](api.API).
		Get("/offers/"+offerId, opts...).
		DoWithDecode(ctx)
}

func main() {
	... 
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()
	
	resp, err := api.GetOffer(ctx, "off_1234", clientx.WithRequestHeaders(map[string][]string{
		"Authorization": []string{"Bearer MY_ACCESS_TOKEN"}, 
	}))
}
Options

There is a list of supported options from the box:

  • WithRateLimit - enables rate limiting mechanism
  • WithRetry - enables backoff retry mechanism
api := New(
	clientx.NewAPI(
		clientx.WithBaseURL("https://php-noise.com"),
		clientx.WithRateLimit(10, 2, time.Minute),
		clientx.WithRetry(10, time.Second*3, time.Minute, clientx.ExponentalBackoff,
			func(resp *http.Response, err error) bool {
				return resp.StatusCode == http.StatusTooManyRequests
			},
		),
	),
)
Rate limiting

Most of the APIs have rate limits. Let's take for example the next limit: 100req/sec, so we want to stay within the limit. The rate limiter functionality supported from the box: wrapper around golang.org/x/time/rate package.

api := New(
	clientx.NewAPI(
		clientx.WithBaseURL("https://php-noise.com"),
		clientx.WithRateLimit(10, 2, time.Minute), // max limit: ^10, burst limit: ^2, interval: ^time.Minute
	),
)

If the limit is exceeded, all further call will be blocked until we gain enough capacity to perform new requests.

Retry

Retry functionality can be combined with rate limiting. There are cases when you don't know the rate limiting interval. In this case you can use backoff retry mechanism. You can retry after 1 sec or you can wait for 60 minutes. The 429 (Too Many Requests) status code is an indicator when rate limit is exceeded.

api := New(
	clientx.NewAPI(
		clientx.WithBaseURL("https://php-noise.com"),
		clientx.WithRateLimit(10, 2, time.Minute), 
		// Parameters: max retry attempts, minimal wait time, maximal wait time, retry function (you could provide your own which is suitable for clientx.RetryFunc), trigger function (in our example we consider all 429 statuses as a tigger)
		clientx.WithRetry(10, time.Second*3, time.Minute, clientx.ExponentalBackoff,
			func(resp *http.Response, err error) bool {
				return resp.StatusCode == http.StatusTooManyRequests
			},
		),
	),
)
Request options

You can add custom headers to request or set query parameters, form data, etc... The list of supported request options you can find here.

func (api *MyAPI) GetOffer(ctx context.Context, offerId string, opts ...clientx.RequestOption) (*Offer, error) {
	return clientx.NewRequestBuilder[struct{}, Offer](api.API).
		Get("/offers/"+offerId, opts...).
		DoWithDecode(ctx)
}

func main() {
    ... 
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()

	resp, err := api.GetOffer(ctx, "off_1234", clientx.WithRequestHeaders(map[string][]string{
		"Authorization":    []string{"Bearer token_test"}, 
		"X-Correlation-Id": []string{"mdj34fjhgsdb4"},
	}))
}
Query parameters encode

There are two ways to encode query parameters, one can be preferred rather than another one.

type GetOfferParams struct {
	FilterBy string `url:"filter_by"`
}

func (param GetOfferParam) Encode(v url.Values) error {
	v.Set("filter_by", param.FilterBy)
	return nil
}


// Variant based on WithQueryParams (when we want to encode through structure tags) 
func (api *MyAPI) GetOffer(ctx context.Context, offerId string, params GetOfferParams, opts ...clientx.RequestOption) (*Offer, error) {
	return clientx.NewRequestBuilder[struct{}, Offer](api.API).
		Get("/offers/"+offerId, opts...).
		WithQueryParams("url", params).
		DoWithDecode(ctx)
}

// Variant based on WithEncodableQueryParams when we implement clientx.ParamEncoder interface
func (api *MyAPI) GetOffer(ctx context.Context, offerId string, params GetOfferParams, opts ...clientx.RequestOption) (*Offer, error) {
	return clientx.NewRequestBuilder[struct{}, Offer](api.API).
		Get("/offers/"+offerId, opts...).
		WithEncodableQueryParams(params).
		DoWithDecode(ctx)
}
Custom encoding & decoding

By default, ClientX uses JSON encoder if not specified. If you want to encode/decode payload and responses in XML or any other formats, you should implement clientx.EncoderDecoder and pass it as a second argument into DoWithDecode function.

func (api *MyAPI) CreateOffer(ctx context.Context, offerId string, body GetOfferParams, opts ...clientx.RequestOption) (*Offer, error) {
	return clientx.NewRequestBuilder[struct{}, Offer](api.API).
		Post("/offers/"+offerId, &body, opts...).
 		WithEncodableQueryParams(params).
		DoWithDecode(ctx, clientx.XMLEncoderDecoder) // selected XML encoder
}

Encoders supported from the box:

  • JSON
  • XML
  • Blank (No actions, no errors)

Contributing

If you found a bug or have an idea for a new feature, please first discuss it with us by submitting a new issue.

Documentation

Overview

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Package clientx provides functions to build and maintain your own HTTP client.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Copyright (c) 2024 0x9ef. All rights reserved. Use of this source code is governed by an MIT license that can be found in the LICENSE file.

Index

Constants

This section is empty.

Variables

View Source
var BlankEncoderDecoder = &blankEncoderDecoder{}

Blank (No Action) Encoder/Decoder realization.

View Source
var ErrRateLimitExceeded = errors.New("rate limit is exceeded")
View Source
var JSONEncoderDecoder = &jsonEncoderDecoder{}

JSON Encoder/Decoder realization.

View Source
var XMLEncoderDecoder = &xmlEncoderDecoder{}

XML Encoder/Decoder realization.

Functions

func ExponentalBackoff

func ExponentalBackoff(attemptNum int, min, max time.Duration) time.Duration

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API represents general base API which has to be inherited.

type DuffelAPI struct {
  *clientx.API
}

func NewAPI

func NewAPI(opts ...Option) *API

NewAPI returns new base API structure with preselected http.DefaultClient and options. Applies all options, overwrites HttpClient if such option is presented.

type Decoder

type Decoder interface {
	Decode(r io.Reader, dst any) error
}

Decoder is a general interface responsibles for decoding responses.

type Empty

type Empty struct{}

Empty is an empty payload for request/response decoding.

type Encoder

type Encoder interface {
	Encode(w io.Writer, v any) error
}

Encoder is a general interface responsibles for encoding payloads.

type EncoderDecoder

type EncoderDecoder interface {
	Encoder
	Decoder
}

type Limiter

type Limiter interface {
	Wait(ctx context.Context) error
	SetBurstAt(at time.Time, burst int)
	SetLimitAt(at time.Time, limit rate.Limit)
}

Limiter is a general interface responsible for rate-limiting functional.

type Option

type Option func(*Options)

func WithBaseURL

func WithBaseURL(url string) Option

WithBaseURL sets base URL to perform requests.

func WithDebug

func WithDebug() Option

WithDebug enables debug logging of requests and responses. DO NOT USE IN PRODUCTION.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient allows you to specify a custom http.Client to use for making requests. This is useful if you want to use a custom transport or proxy.

func WithHeader

func WithHeader(key string, value string) Option

WithHeader sets global header. Overwrites values related to key.

func WithHeaderSet

func WithHeaderSet(headers map[string][]string) Option

WithHeaderSet sets global headers. Overwrites previously defined header set.

func WithRateLimit

func WithRateLimit(limit int, burst int, per time.Duration) Option

WithRateLimit sets burst and limit for a ratelimiter.

func WithRetry

func WithRetry(maxAttempts int, minWaitTime, maxWaitTime time.Duration, f RetryFunc, conditions ...RetryCond) Option

WithRetry sets custom retrier implementation. Also enables retrying mechanism. If f retry function isn't provided ExponentalBackoff algorithm will be used.

type OptionRateLimit

type OptionRateLimit struct {
	Limit int
	Burst int
	// Per allows configuring limits for different time windows.
	Per time.Duration
}

type OptionRetry

type OptionRetry struct {
	MaxAttempts int
	MinWaitTime time.Duration
	MaxWaitTime time.Duration
	// Conditions that will be applied to retry mechanism.
	Conditions []RetryCond
	// Retry function which will be used as main retry logic.
	Fn RetryFunc
}

type Options

type Options struct {
	BaseURL    string
	HttpClient *http.Client
	Headers    http.Header
	// Debug prints responses into os.Stdout.
	Debug bool
	// RateLimitParseFn is a custom function that parses rate limits from HTTP response.
	// For example from X-Ratelimit-Limit, X-Ratelimit-Remaining headers.
	RateLimitParseFn func(*http.Response) (limit int, remaining int, resetAt time.Time, err error)
	RateLimit        *OptionRateLimit
	Retry            *OptionRetry
}

type ParamEncoder

type ParamEncoder[T any] interface {
	Encode(v url.Values) error
}

func NormalizeParams

func NormalizeParams[T ParamEncoder[T]](params []T) []ParamEncoder[T]

type RequestBuilder

type RequestBuilder[Req any, Resp any] struct {
	// contains filtered or unexported fields
}

func NewRequestBuilder

func NewRequestBuilder[Req any, Resp any](api *API) *RequestBuilder[Req, Resp]

NewRequestBuilder creates a new request builder from API for designated Req, Resp. Default method is GET. If you want to specify method, you should call corresponding Get/Post/Put/Patch/Delete methods.

func (*RequestBuilder[Req, Resp]) AfterResponse

func (rb *RequestBuilder[Req, Resp]) AfterResponse(f func(resp *http.Response, decoded *Resp) error) *RequestBuilder[Req, Resp]

AfterResponse adds to a chain function that will be executed after response is obtained. Note! The second argument (decoded) in f function is only available when using DoWithDecode method to perform request.

func (*RequestBuilder[Req, Resp]) Connect

func (rb *RequestBuilder[Req, Resp]) Connect(path string, opts ...RequestOption) *RequestBuilder[Req, Resp]

Connect builds CONNECT request. Appends request options.

func (*RequestBuilder[Req, Resp]) Delete

func (rb *RequestBuilder[Req, Resp]) Delete(path string, body *Req, opts ...RequestOption) *RequestBuilder[Req, Resp]

Delete builds DELETE request with specified body (if any). Appends request options.

func (*RequestBuilder[Req, Resp]) Do

func (rb *RequestBuilder[Req, Resp]) Do(ctx context.Context) (*http.Response, error)

Do executes request and returns *http.Response. Returns error if any.

func (*RequestBuilder[Req, Resp]) DoWithDecode

func (rb *RequestBuilder[Req, Resp]) DoWithDecode(ctx context.Context, enc ...EncoderDecoder) (*Resp, error)

DoWithDecode executes request and decodes response into Resp object. Returns error if any.

func (*RequestBuilder[Req, Resp]) Get

func (rb *RequestBuilder[Req, Resp]) Get(path string, opts ...RequestOption) *RequestBuilder[Req, Resp]

Get builds GET request with no body specified. Appends request options (includes request options that were specified at NewRequestBuilder).

func (*RequestBuilder[Req, Resp]) Head

func (rb *RequestBuilder[Req, Resp]) Head(path string, opts ...RequestOption) *RequestBuilder[Req, Resp]

Head builds HEAD request. Appends request options.

func (*RequestBuilder[Req, Resp]) Options

func (rb *RequestBuilder[Req, Resp]) Options(path string, opts ...RequestOption) *RequestBuilder[Req, Resp]

Options builds OPTIONS request. Appends request options.

func (*RequestBuilder[Req, Resp]) Patch

func (rb *RequestBuilder[Req, Resp]) Patch(path string, body *Req, opts ...RequestOption) *RequestBuilder[Req, Resp]

Patch builds PATCH request with specified body (if any). Appends request options.

func (*RequestBuilder[Req, Resp]) Post

func (rb *RequestBuilder[Req, Resp]) Post(path string, body *Req, opts ...RequestOption) *RequestBuilder[Req, Resp]

Post builds POST request with specified body. Appends request options.

func (*RequestBuilder[Req, Resp]) Put

func (rb *RequestBuilder[Req, Resp]) Put(path string, body *Req, opts ...RequestOption) *RequestBuilder[Req, Resp]

Put builds PUT request with specified body (if any). Appends request options.

func (*RequestBuilder[Req, Resp]) Trace

func (rb *RequestBuilder[Req, Resp]) Trace(path string, opts ...RequestOption) *RequestBuilder[Req, Resp]

Trace builds TRACE request. Appends request options.

func (*RequestBuilder[Req, Resp]) WithEncodableQueryParams

func (rb *RequestBuilder[Req, Resp]) WithEncodableQueryParams(params ...ParamEncoder[Req]) *RequestBuilder[Req, Resp]

WithEncodableQueryParams sets URL query parameters from structure which implements ParamEncoder interface.

func (*RequestBuilder[Req, Resp]) WithErrorDecode

func (rb *RequestBuilder[Req, Resp]) WithErrorDecode(f func(resp *http.Response) (bool, error)) *RequestBuilder[Req, Resp]

WithErrorDecode sets custom error decoding function. Will be executed immediately after request is performed.

func (*RequestBuilder[Req, Resp]) WithForm

func (rb *RequestBuilder[Req, Resp]) WithForm(obj url.Values) *RequestBuilder[Req, Resp]

WithForm sets the form data for the request.

func (*RequestBuilder[Req, Resp]) WithQueryParams

func (rb *RequestBuilder[Req, Resp]) WithQueryParams(tag string, params ...Req) *RequestBuilder[Req, Resp]

WithQueryParams sets URL query parameters from structure by accesing field with provided tag alias.

type RequestOption

type RequestOption func(req *http.Request) error

func WithRequestForm

func WithRequestForm(form url.Values) RequestOption

func WithRequestHeaders

func WithRequestHeaders(headers map[string][]string) RequestOption

func WithRequestQueryEncodableParams

func WithRequestQueryEncodableParams[T any](params ...ParamEncoder[T]) RequestOption

WithRequestQueryEncodableParams encodes query params by implementing ParamEncoder[T] interface, calls Encode(url.Values) functional to set query params.

func WithRequestQueryParams

func WithRequestQueryParams[T any](tag string, params ...T) RequestOption

WithRequestQueryParams encodes query params automatically by accesing fields with custom tag.

type Retrier

type Retrier interface {
	Next() time.Duration
	Reset() int64
	Attempt() int64
}

Retrier is a general interface for custom retry algo implementations.

type RetryCond

type RetryCond func(resp *http.Response, err error) bool

RetryCond is a condition that applies only to retry backoff mechanism.

type RetryFunc

type RetryFunc func(n int, min, max time.Duration) time.Duration

RetryFunc takes attemps number, minimal and maximal wait time for backoff. Returns duration that mechanism have to wait before making a request.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL