Skip to content

Commit

Permalink
Improving Paginators by making them aware of BindingParams #2
Browse files Browse the repository at this point in the history
- Added the reddit.RateLimit type for tracking rate limits in the reddit API, as well as the RateLimitsConfig interface for configuring rates (13/03/2023 - 15:16:26)
- A lot more reddit.Types taken mostly from https://github.com/vartanbeno/go-reddit/reddit/things.go (13/03/2023 - 15:23:14)
- Paginator's are now aware of multiple different types of pagination parameters thanks to the Binding.Params method (14/03/2023 - 14:14:10)
- The untyped paginator is now the type Paginator[any, any] which makes more sense and allows us to actually use the Afterable interafce (14/03/2023 - 16:22:05)
  • Loading branch information
andygello555 committed Mar 14, 2023
1 parent 407c633 commit 796d66d
Show file tree
Hide file tree
Showing 13 changed files with 1,000 additions and 248 deletions.
80 changes: 74 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/machinebox/graphql"
"net/http"
"reflect"
"sync"
"time"
)

Expand Down Expand Up @@ -37,8 +38,48 @@ type Request interface {

// Client is the API client that will execute the Binding.
type Client interface {
// Run should execute the given Request and unmarshal the response into the given response interface.
Run(ctx context.Context, attrs map[string]any, req Request, res any) error
// Run should execute the given Request and unmarshal the response into the given response interface. It is usually
// called from Binding.Execute to execute a Binding, hence why we also pass in the name of the Binding (from
// Binding.Name).
Run(ctx context.Context, bindingName string, attrs map[string]any, req Request, res any) error
}

type RateLimitType int

const (
// RequestRateLimit means that the RateLimit is limited by the number of HTTP requests that can be made in a certain
// timespan.
RequestRateLimit RateLimitType = iota
// ResourceRateLimit means that the RateLimit is limited by the number of resources that can be fetched in a certain
// timespan.
ResourceRateLimit
)

// RateLimit represents a RateLimit for a binding.
type RateLimit interface {
// Reset returns the time at which the RateLimit resets.
Reset() time.Time
// Remaining returns the number of requests remaining/resources that can be fetched for this RateLimit.
Remaining() int
// Used returns the number of requests used/resources fetched so far for this RateLimit.
Used() int
// Type is the type of the RateLimit. See RateLimitType for documentation.
Type() RateLimitType
}

// RateLimitedClient is an API Client that has a RateLimit for each Binding it has authority over.
type RateLimitedClient interface {
// Client should implement a Client.Run method that sets an internal sync.Map of RateLimit(s).
Client
// RateLimits returns the sync.Map of Binding names to RateLimit instances.
RateLimits() *sync.Map
// AddRateLimit should add a RateLimit to the internal sync.Map within the Client. It should check if the Binding of
// the given name already has a RateLimit, and whether the RateLimit.Reset lies after the currently set RateLimit
// for that Binding.
AddRateLimit(bindingName string, rateLimit RateLimit)
// LatestRateLimit should return the latest RateLimit for the Binding of the given name. If multiple Binding(s)
// share the same RateLimit(s) then this can also be encoded into this method.
LatestRateLimit(bindingName string) RateLimit
}

// BindingWrapper wraps a Binding value with its name. This is used within the Schema map so that we don't have to use
Expand All @@ -54,11 +95,23 @@ func (bw BindingWrapper) String() string {
return fmt.Sprintf("%s/%v", bw.name, bw.binding.Type())
}

// Name returns the name of the underlying Binding.
func (bw BindingWrapper) Name() string { return bw.name }

func (bw BindingWrapper) bindingName() string {
return bw.binding.MethodByName("Name").Call([]reflect.Value{})[0].Interface().(string)
}

// Paginated calls the Binding.Paginated method for the underlying Binding in the BindingWrapper.
func (bw BindingWrapper) Paginated() bool {
return bw.binding.MethodByName("Paginated").Call([]reflect.Value{})[0].Bool()
}

// Paginator returns an un-typed Paginator for the underlying Binding of the BindingWrapper.
func (bw BindingWrapper) Paginator(client Client, waitTime time.Duration, args ...any) (paginator Paginator[any, any], err error) {
return NewPaginator(client, waitTime, bw, args...)
}

// ArgsFromStrings calls the Binding.ArgsFromStrings method for the underlying Binding in the BindingWrapper.
func (bw BindingWrapper) ArgsFromStrings(args ...string) (parsedArgs []any, err error) {
values := bw.binding.MethodByName("ArgsFromStrings").Call(slices.Comprehension(args, func(idx int, value string, arr []string) reflect.Value {
Expand All @@ -72,6 +125,11 @@ func (bw BindingWrapper) ArgsFromStrings(args ...string) (parsedArgs []any, err
return
}

// Params calls the Binding.Params method for the underlying Binding in the BindingWrapper.
func (bw BindingWrapper) Params() []BindingParam {
return bw.binding.MethodByName("Params").Call([]reflect.Value{})[0].Interface().([]BindingParam)
}

// Execute calls the Binding.Execute method for the underlying Binding in the BindingWrapper.
func (bw BindingWrapper) Execute(client Client, args ...any) (val any, err error) {
arguments := []any{client}
Expand All @@ -87,14 +145,20 @@ func (bw BindingWrapper) Execute(client Client, args ...any) (val any, err error
return
}

// WrapBinding will return the BindingWrapper for the given Binding of the given name.
func WrapBinding[ResT any, RetT any](name string, binding Binding[ResT, RetT]) BindingWrapper {
func (bw BindingWrapper) setName(name string) {
fmt.Println("setName", bw.binding.Type())
bw.binding.MethodByName("SetName").Call([]reflect.Value{reflect.ValueOf(name)})
}

// WrapBinding will return the BindingWrapper for the given Binding. The name of the BindingWrapper will be fetched from
// Binding.Name, so make sure to override this before using the Binding.
func WrapBinding[ResT any, RetT any](binding Binding[ResT, RetT]) BindingWrapper {
var (
resT ResT
retT RetT
)
return BindingWrapper{
name: name,
name: binding.Name(),
responseType: reflect.TypeOf(resT),
returnType: reflect.TypeOf(retT),
binding: reflect.ValueOf(&binding).Elem(),
Expand All @@ -112,6 +176,10 @@ type API struct {

// NewAPI constructs a new API instance for the given Client and Schema combination.
func NewAPI(client Client, schema Schema) *API {
for bindingName, bindingWrapper := range schema {
bindingWrapper.name = bindingName
}

return &API{
Client: client,
schema: schema,
Expand Down Expand Up @@ -152,7 +220,7 @@ func (api *API) Execute(name string, args ...any) (val any, err error) {
}

// Paginator returns a Paginator for the Binding of the given name within the API.
func (api *API) Paginator(name string, waitTime time.Duration, args ...any) (paginator Paginator[[]any, []any], err error) {
func (api *API) Paginator(name string, waitTime time.Duration, args ...any) (paginator Paginator[any, any], err error) {
var binding BindingWrapper
if binding, err = api.checkBindingExists(name); err != nil {
return
Expand Down
6 changes: 3 additions & 3 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
type httpClient struct {
}

func (h httpClient) Run(ctx context.Context, attrs map[string]any, req Request, res any) (err error) {
func (h httpClient) Run(ctx context.Context, bindingName string, attrs map[string]any, req Request, res any) (err error) {
request := req.(HTTPRequest).Request

var response *http.Response
Expand Down Expand Up @@ -496,12 +496,12 @@ func ExampleNewAPI() {
// when creating Bindings. This will execute a similar HTTP request to the "products" Binding but
// Binding.Execute will instead return a single Product instance.
// Note: how the RetT type param is set to just "Product".
"first_product": WrapBinding("first_product", NewBindingChain(func(binding Binding[[]Product, Product], args ...any) (request Request) {
"first_product": WrapBinding(NewBindingChain(func(binding Binding[[]Product, Product], args ...any) (request Request) {
req, _ := http.NewRequest(http.MethodGet, "https://fakestoreapi.com/products?limit=1", nil)
return HTTPRequest{req}
}).SetResponseMethod(func(binding Binding[[]Product, Product], response []Product, args ...any) Product {
return response[0]
})),
}).SetName("first_product")),
})

// Then we can execute our "users" binding with a limit of 3...
Expand Down
34 changes: 30 additions & 4 deletions api/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ type Binding[ResT any, RetT any] interface {
// chained with others when creating a new Binding through NewBindingChain.
SetPaginated(paginated bool) Binding[ResT, RetT]

// Name returns the name of the Binding. When using NewBinding, NewBindingChain, or NewWrappedBinding, this will be
// set to whatever is returned by the following line of code:
// fmt.Sprintf("%T", binding)
// Where "binding" is the referred to Binding.
Name() string
// SetName sets the name of the Binding. This returns the Binding so it can be chained.
SetName(name string) Binding[ResT, RetT]

// Attrs returns the attributes for the Binding. These can be passed in when creating a Binding through the
// NewBinding function. Attrs can be used in any of the implemented functions, and they are also passed to
// Client.Run when Execute-ing the Binding.
Expand Down Expand Up @@ -115,6 +123,8 @@ type bindingProto[ResT any, RetT any] struct {
checkedParams bool
paramsMethod BindingParamsMethod[ResT, RetT]
paginated bool
name string
nameSet bool
attrs map[string]any
attrFuncs []Attr
}
Expand Down Expand Up @@ -244,8 +254,8 @@ func checkParams(params []BindingParam) (err error) {

// checkParams will see if the given BindingParam(s) make sense. This means that:
// - BindingParam(s) should have a unique BindingParam.name.
// - Non Required BindingParam(s) should trail after all Required BindingParam(s).
// - Variadic BindingParam(s) should trail after all non-required BindingParam(s)
// - Non Required BindingParam(s) should trail afterParamSet all Required BindingParam(s).
// - Variadic BindingParam(s) should trail afterParamSet all non-required BindingParam(s)
// - Variadic BindingParam(s) should not be Required.
// - Variadic BindingParam(s) should have DefaultValue that is an empty reflect.Slice/reflect.Array type.
//
Expand Down Expand Up @@ -387,7 +397,7 @@ func (b bindingProto[ResT, RetT]) Execute(client Client, args ...any) (response
responseWrapperInt := responseWrapper.Interface()

ctx := context.Background()
if err = client.Run(ctx, b.attrs, req, &responseWrapperInt); err != nil {
if err = client.Run(ctx, b.Name(), b.attrs, req, &responseWrapperInt); err != nil {
err = errors.Wrapf(err, "could not Execute Binding %T", b)
return
}
Expand All @@ -406,6 +416,20 @@ func (b bindingProto[ResT, RetT]) SetPaginated(paginated bool) Binding[ResT, Ret
b.paginated = paginated
return &b
}

func (b bindingProto[ResT, RetT]) Name() string {
if !b.nameSet {
return fmt.Sprintf("%T", b)
}
return b.name
}

func (b bindingProto[ResT, RetT]) SetName(name string) Binding[ResT, RetT] {
b.name = name
b.nameSet = true
return &b
}

func (b bindingProto[ResT, RetT]) Attrs() map[string]any { return b.attrs }

func (b bindingProto[ResT, RetT]) AddAttrs(attrs ...Attr) Binding[ResT, RetT] {
Expand Down Expand Up @@ -516,7 +540,9 @@ func NewWrappedBinding[ResT any, RetT any](
paginated bool,
attrs ...Attr,
) BindingWrapper {
return WrapBinding(name, NewBinding(request, wrap, unwrap, response, params, paginated, attrs...))
b := NewBinding(request, wrap, unwrap, response, params, paginated, attrs...)
b.SetName(name)
return WrapBinding(b)
}

// NewBindingChain creates a new Binding for an API via a prototype that implements the Binding interface. Unlike the
Expand Down

0 comments on commit 796d66d

Please sign in to comment.