api

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2023 License: MIT Imports: 13 Imported by: 4

README

gapi

An agnostic wrapper for creating Go bindings to all your favourite web APIs.

Why

Have you ever ran into these problems when developing a Go project that uses obscure/new web APIs?

  1. You want to use a web API, but are no Go clients/bindings/packages available for it
  2. Maybe there is one, but it is large/complex, and you only really need a subset of its functionality

Then gapi is for you!

What does it do

  1. Makes it easy to create bindings for actions within that web API
  2. Makes it easy to use paginated API actions, even with rate limits!
  3. Type checked arguments can be passed to every binding that you create which will cause appropriate errors at runtime
  4. The entire request pipeline for a binding can be easily modified at any point
  5. Supports Go generics so that you can create bindings that will return type-checked values at compile time!

Example API

The following example defines an API client + bindings for the Fake Store REST API.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/andygello555/agem"
	"github.com/andygello555/gapi"
	"github.com/pkg/errors"
	"io"
	"net/http"
	"net/url"
	"strconv"
)

// Product defines a product returned by the fakestoreapi.
type Product struct {
	ID          int     `json:"id"`
	Title       string  `json:"title"`
	Price       float64 `json:"price"`
	Category    string  `json:"category"`
	Description string  `json:"description"`
	Image       string  `json:"image"`
}

// User defines a user returned by the fakestoreapi.
type User struct {
	ID       int    `json:"id"`
	Email    string `json:"email"`
	Username string `json:"username"`
	Password string `json:"password"`
	Name     struct {
		Firstname string `json:"firstname"`
		Lastname  string `json:"lastname"`
	} `json:"name"`
	Address struct {
		City        string `json:"city"`
		Street      string `json:"street"`
		Number      int    `json:"number"`
		Zipcode     string `json:"zipcode"`
		Geolocation struct {
			Lat  string `json:"lat"`
			Long string `json:"long"`
		} `json:"geolocation"`
	} `json:"address"`
	Phone string `json:"phone"`
}

type httpClient struct {
	*http.Client
}

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

	var response *http.Response
	if response, err = h.Do(request); err != nil {
		return err
	}

	if response.Body != nil {
		defer func(body io.ReadCloser) {
			err = agem.MergeErrors(err, errors.Wrapf(body.Close(), "could not close response body to %s", request.URL.String()))
		}(response.Body)
	}

	var body []byte
	if body, err = io.ReadAll(response.Body); err != nil {
		err = errors.Wrapf(err, "could not read response body to %s", request.URL.String())
		return
	}

	err = json.Unmarshal(body, res)
	return
}

func main() {
	// Then we create a Client instance. Here httpClient is a type that implements the Client interface, where
	// Client.Run performs an HTTP request using http.DefaultClient, and then unmarshals the JSON response into the
	// response wrapper.
	client := httpClient{http.DefaultClient}

	// Finally, we create the API itself by creating and registering all our Bindings within the Schema using the
	// NewWrappedBinding method. The "users" and "products" Bindings take only one argument: the limit argument. This
	// limits the number of resources returned by the fakestoreapi. This is applied to the Request by setting the query
	// params for the http.Request.
	a := api.NewAPI(client, api.Schema{
		// Note: we do not supply a wrap and an unwrap method for the "users" and "products" Bindings because the
		//       fakestoreapi returns JSON that can be unmarshalled straight into an appropriate instance of type ResT.
		//       We also don't need to supply a response method because the ResT type is the same as the RetT type.
		"users": api.NewWrappedBinding("users",
			func(b api.Binding[[]User, []User], args ...any) (request api.Request) {
				u, _ := url.Parse("https://fakestoreapi.com/users")
				if len(args) > 0 {
					query := u.Query()
					query.Add("limit", strconv.Itoa(args[0].(int)))
					u.RawQuery = query.Encode()
				}
				req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
				return api.HTTPRequest{req}
			}, nil, nil, nil,
			func(binding api.Binding[[]User, []User]) []api.BindingParam {
				return api.Params("limit", 1)
			}, false,
		),
		"products": api.NewWrappedBinding("products",
			func(b api.Binding[[]Product, []Product], args ...any) api.Request {
				u, _ := url.Parse("https://fakestoreapi.com/products")
				if len(args) > 0 {
					query := u.Query()
					query.Add("limit", strconv.Itoa(args[0].(int)))
					u.RawQuery = query.Encode()
				}
				req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
				return api.HTTPRequest{req}
			}, nil, nil, nil,
			func(binding api.Binding[[]Product, []Product]) []api.BindingParam {
				return api.Params("limit", 1)
			}, false,
		),
		// The "first_product" Binding showcases how to set the response method, as well as how to use the chaining API
		// 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": api.WrapBinding(api.NewBindingChain(func(binding api.Binding[[]Product, Product], args ...any) (request api.Request) {
			req, _ := http.NewRequest(http.MethodGet, "https://fakestoreapi.com/products?limit=1", nil)
			return api.HTTPRequest{req}
		}).SetResponseMethod(func(binding api.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...
	var resp any
	var err error
	if resp, err = a.Execute("users", 3); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(resp.([]User))

	// ...and we can also execute our "products" binding with a limit of 1...
	if resp, err = a.Execute("products", 1); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(resp.([]Product))

	// ...and we can also execute our "first_product" binding.
	if resp, err = a.Execute("first_product"); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(resp.(Product))

	// Finally, we will check whether the "limit" parameter for the "users" action defaults to 1
	if resp, err = a.Execute("users"); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(resp.([]User))
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type API

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

API represents a connection to an API with multiple different available Binding(s).

func NewAPI

func NewAPI(client Client, schema Schema) *API

NewAPI constructs a new API instance for the given Client and Schema combination.

Example
// First we need to define our API's response and return structures.
type Product struct {
	ID          int     `json:"id"`
	Title       string  `json:"title"`
	Price       float64 `json:"price"`
	Category    string  `json:"category"`
	Description string  `json:"description"`
	Image       string  `json:"image"`
}

type User struct {
	ID       int    `json:"id"`
	Email    string `json:"email"`
	Username string `json:"username"`
	Password string `json:"password"`
	Name     struct {
		Firstname string `json:"firstname"`
		Lastname  string `json:"lastname"`
	} `json:"name"`
	Address struct {
		City        string `json:"city"`
		Street      string `json:"street"`
		Number      int    `json:"number"`
		Zipcode     string `json:"zipcode"`
		Geolocation struct {
			Lat  string `json:"lat"`
			Long string `json:"long"`
		} `json:"geolocation"`
	} `json:"address"`
	Phone string `json:"phone"`
}

// Then we create a Client instance. Here httpClient is a type that implements the Client interface, where
// Client.Run performs an HTTP request using http.DefaultClient, and then unmarshals the JSON response into the
// response wrapper.
client := httpClient{URL: "https://fakestoreapi.com"}

// Finally, we create the API itself by creating and registering all our Bindings within the Schema using the
// NewWrappedBinding method. The "users" and "products" Bindings take only one argument: the limit argument. This
// limits the number of resources returned by the fakestoreapi. This is applied to the Request by setting the query
// params for the http.Request.
api := NewAPI(client, Schema{
	// Note: we do not supply a wrap and an unwrap method for the "users" and "products" Bindings because the
	//       fakestoreapi returns JSON that can be unmarshalled straight into an appropriate instance of type ResT.
	//       We also don't need to supply a response method because the ResT type is the same as the RetT type.
	"users": NewWrappedBinding("users",
		func(b Binding[[]User, []User], args ...any) (request Request) {
			u, _ := url.Parse("https://fakestoreapi.com/users")
			if len(args) > 0 {
				query := u.Query()
				query.Add("limit", strconv.Itoa(args[0].(int)))
				u.RawQuery = query.Encode()
			}
			req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
			return HTTPRequest{req}
		}, nil, nil, nil,
		func(binding Binding[[]User, []User]) []BindingParam {
			return Params("limit", 1)
		}, false,
	),
	"products": NewWrappedBinding("products",
		func(b Binding[[]Product, []Product], args ...any) Request {
			u, _ := url.Parse("https://fakestoreapi.com/products")
			if len(args) > 0 {
				query := u.Query()
				query.Add("limit", strconv.Itoa(args[0].(int)))
				u.RawQuery = query.Encode()
			}
			req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
			return HTTPRequest{req}
		}, nil, nil, nil,
		func(binding Binding[[]Product, []Product]) []BindingParam {
			return Params("limit", 1)
		}, false,
	),
	// The "first_product" Binding showcases how to set the response method, as well as how to use the chaining API
	// when creating Bindings. This will execute a similar HTTP request to the "products" Binding but
	// Binding.Execute will instead return a single Product instance. We can also add more attributes to the Binding
	// which we can access at any point from the Binding instance.
	// Note: how the RetT type param is set to just "Product".
	"first_product": WrapBinding(NewBindingChain(func(binding Binding[[]Product, Product], args ...any) (request Request) {
		client := binding.Attrs()["client"].(httpClient)
		req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/products?limit=1", client.URL), nil)
		return HTTPRequest{req}
	}).SetResponseMethod(func(binding Binding[[]Product, Product], response []Product, args ...any) Product {
		return response[0]
	}).SetName("first_product").AddAttrs(func(client Client) (string, any) {
		return "client", client.(httpClient)
	})),
})

// Then we can execute our "users" binding with a limit of 3...
var resp any
var err error
if resp, err = api.Execute("users", 3); err != nil {
	fmt.Println(err)
	return
}
fmt.Println(resp.([]User))

// ...and we can also execute our "products" binding with a limit of 1...
if resp, err = api.Execute("products", 1); err != nil {
	fmt.Println(err)
	return
}
fmt.Println(resp.([]Product))

// ...and we can also execute our "first_product" binding.
if resp, err = api.Execute("first_product"); err != nil {
	fmt.Println(err)
	return
}
fmt.Println(resp.(Product))

// Finally, we will check whether the "limit" parameter for the "users" action defaults to 1
if resp, err = api.Execute("users"); err != nil {
	fmt.Println(err)
	return
}
fmt.Println(resp.([]User))
Output:

[{1 john@gmail.com johnd m38rmF$ {john doe} {kilcoole new road 7682 12926-3874 {-37.3159 81.1496}} 1-570-236-7033} {2 morrison@gmail.com mor_2314 83r5^_ {david morrison} {kilcoole Lovers Ln 7267 12926-3874 {-37.3159 81.1496}} 1-570-236-7033} {3 kevin@gmail.com kevinryan kev02937@ {kevin ryan} {Cullman Frances Ct 86 29567-1452 {40.3467 -30.1310}} 1-567-094-1345}]
[{1 Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops 109.95 men's clothing Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg}]
{1 Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops 109.95 men's clothing Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg}
[{1 john@gmail.com johnd m38rmF$ {john doe} {kilcoole new road 7682 12926-3874 {-37.3159 81.1496}} 1-570-236-7033}]

func (*API) ArgsFromStrings

func (api *API) ArgsFromStrings(name string, args ...string) (parsedArgs []any, err error)

ArgsFromStrings will execute the Binding.ArgsFromStrings method for the Binding of the given name within the API.

func (*API) Binding

func (api *API) Binding(name string) (BindingWrapper, bool)

Binding returns the BindingWrapper with the given name in the Schema for this API. The second return value is an "ok" flag.

func (*API) Execute

func (api *API) Execute(name string, args ...any) (val any, err error)

Execute will execute the Binding of the given name within the API.

func (*API) Paginator

func (api *API) Paginator(name string, waitTime time.Duration, args ...any) (paginator Paginator[any, any], err error)

Paginator returns a Paginator for the Binding of the given name within the API.

type Afterable

type Afterable interface {
	// After returns the value of the "after" parameter that should be used for the next page of pagination. If this
	// returns nil, then it is assumed that pagination has finished.
	After() any
}

Afterable denotes whether a response type can be used in a Paginator for a Binding that takes an "after" parameter.

type Attr

type Attr func(client Client) (string, any)

Attr is an attribute that can be passed to a Binding when using the NewBinding method. It should return a string key and a value.

type Binding

type Binding[ResT any, RetT any] interface {
	// Request constructs the Request that will be sent to the API using a Client. The function can take multiple
	// arguments that should be handled accordingly and passed to the Request. These are the same arguments passed in
	// from the Binding.Execute method.
	Request(args ...any) (request Request)
	// GetRequestMethod returns the BindingRequestMethod that is called when Binding.Request is called. This is useful
	// when you want to reuse a BindingRequestMethod for another Binding.
	GetRequestMethod() BindingRequestMethod[ResT, RetT]

	// ResponseWrapper should create a wrapper for the given response type (ResT) and return the pointer reflect.Value to
	// this wrapper. Client.Run will then unmarshal the response into this wrapper instance. This is useful for APIs
	// that return the actual resource (of type ResT) within a structure that is nested within the returned response.
	ResponseWrapper(args ...any) (responseWrapper reflect.Value, err error)
	// GetResponseWrapperMethod returns the BindingResponseWrapperMethod that is called when Binding.Response is called.
	// This is helpful when you want to reuse a BindingResponseWrapperMethod for another Binding.
	GetResponseWrapperMethod() BindingResponseWrapperMethod[ResT, RetT]
	// SetResponseWrapperMethod sets the BindingResponseWrapperMethod that is called when Binding.ResponseWrapper is
	// called. This enables chaining when creating a Binding through NewBindingChain.
	SetResponseWrapperMethod(method BindingResponseWrapperMethod[ResT, RetT]) Binding[ResT, RetT]

	// ResponseUnwrapped should unwrap the response that was made to the API after Client.Run. This should return an
	// instance of the ResT type.
	ResponseUnwrapped(responseWrapper reflect.Value, args ...any) (response ResT, err error)
	// GetResponseUnwrappedMethod returns the BindingResponseUnwrappedMethod that is called when
	// Binding.ResponseUnwrapped is called. This is useful when you want to reuse a BindingResponseUnwrappedMethod for
	// another Binding.
	GetResponseUnwrappedMethod() BindingResponseUnwrappedMethod[ResT, RetT]
	// SetResponseUnwrappedMethod sets the BindingResponseUnwrappedMethod that is called when Binding.ResponseWrapper is
	// called. This enables chaining when creating a Binding through NewBindingChain.
	SetResponseUnwrappedMethod(method BindingResponseUnwrappedMethod[ResT, RetT]) Binding[ResT, RetT]

	// Response converts the response from the API from the type ResT to the type RetT. It should also be passed
	// additional arguments from Execute.
	Response(response ResT, args ...any) RetT
	// GetResponseMethod returns the BindingResponseMethod that is called when Binding.Response is called. This is
	// useful when you want to reuse a BindingResponseMethod for another Binding.
	GetResponseMethod() BindingResponseMethod[ResT, RetT]
	// SetResponseMethod sets the BindingResponseMethod that is called when Binding.Response is called. This enables
	// chaining when creating a Binding through NewBindingChain.
	SetResponseMethod(method BindingResponseMethod[ResT, RetT]) Binding[ResT, RetT]

	// Params returns the BindingParam(s) that this Binding's Execute method takes. These BindingParam(s) are used for
	// type-checking each argument passed to Execute. If no BindingParam(s) are returned by Params, then no
	// type-checking will be performed in Execute.
	Params() []BindingParam
	// GetParamsMethod returns the BindingParamsMethod that is called when Binding.Params is called. This is useful when
	// you want to reuse a BindingParamsMethod for another Binding.
	GetParamsMethod() BindingParamsMethod[ResT, RetT]
	// SetParamsMethod sets the BindingParamsMethod that is called when Binding.Params is called. This enables chaining
	// when creating a Binding through NewBindingChain.
	SetParamsMethod(method BindingParamsMethod[ResT, RetT]) Binding[ResT, RetT]
	// ArgsFromStrings parses the given list of string arguments into their required types for the Params of the
	// Binding.
	ArgsFromStrings(args ...string) ([]any, error)

	// Execute will execute the BindingWrapper using the given Client and arguments. It returns the response converted to RetT
	// using the Response method, as well as an error that could have occurred.
	Execute(client Client, args ...any) (response RetT, err error)

	// Paginated returns whether the Binding is paginated.
	Paginated() bool
	// SetPaginated sets whether the Binding is paginated. It also returns the Binding so that this method can be
	// 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.
	Attrs() map[string]any
	// AddAttrs adds the given Attr functions to the Binding. Each given Attr will attempt to be evaluated when AddAttrs
	// is called. However, when evaluating these Attr functions the Client param will be passed in as nil. If some of
	// these can't be evaluated, due to the lack of the Client param, then these unevaluated Attr functions will also be
	// evaluated at the start of the Binding.Execute method, this time with the Client that is passed to that method.
	AddAttrs(attrs ...Attr) Binding[ResT, RetT]
}

Binding represents an action in an API that can be executed. It takes two type parameters:

• ResT (Response Type): the type used when unmarshalling the response from the API. Be sure to annotate this with the correct field tags if necessary.

• RetT (Return Type): the type that will be returned from the Response method. This is the cleaned type that will be returned to the user.

To create a new Binding refer to the NewBinding and NewWrappedBinding methods.

func NewBinding

func NewBinding[ResT any, RetT any](
	request BindingRequestMethod[ResT, RetT],
	wrap BindingResponseWrapperMethod[ResT, RetT],
	unwrap BindingResponseUnwrappedMethod[ResT, RetT],
	response BindingResponseMethod[ResT, RetT],
	params BindingParamsMethod[ResT, RetT],
	paginated bool,
	attrs ...Attr,
) Binding[ResT, RetT]

NewBinding creates a new Binding for an API via a prototype that implements the Binding interface. The following parameters must be provided:

• request: the method used to construct the Request that will be sent to the API using Client.Run. This will implement the Binding.Request method. The function takes the Binding, from which Binding.Attrs can be accessed, as well as taking multiple arguments that should be handled accordingly. These are the same arguments passed in from the Binding.Execute method. This parameter cannot be supplied a nil-pointer.

• wrap: the method used to construct the wrapper for the response, before it is passed to Client.Run. This will implement the Binding.ResponseWrapper method. The function takes the Binding, from which Binding.Attrs can be accessed, as well as taking multiple arguments, passed in from Binding.Execute, that can be used in any way the user requires. When supplied a nil-pointer, Binding.ResponseWrapper will construct a wrapper instance which is a pointer type to ResT.

• unwrap: the method used to unwrap the wrapper instance, constructed by Binding.ResponseWrap, after Client.Run has been executed. This will implement the Binding.ResponseUnwrapped method. The function takes the Binding, the response wrapper as a reflect.Value instance, and the arguments that were passed into Binding.Execute. When supplied a nil-pointer, Binding.ResponseUnwrapped will assert the wrapper instance into *ResT and then return the referenced value of this asserted pointer-type. This should only be nil if the wrap argument is also nil.

• response: the method used to convert the response from Binding.ResponseUnwrapped from the type ResT to the type RetT. This implements the Binding.Response method. If this is nil, then when executing the Binding.Response method the response will be cast to any then asserted into the RetT type. This is useful when the response type is the same as the return type.

• params: the method used to return the BindingParam(s) for type-checking the arguments passed to the Binding.Execute method. This implements the Binding.Params method. If this is nil, then Binding.Params will return an empty list of BindingParam(s), which disables the type-checking performed in Binding.Execute.

• paginated: indicates whether this Binding is paginated. If a Binding is paginated, then it can be used with a typedPaginator instance to find all/some resources for that Binding. When creating a paginated Binding make sure to bind first argument of the request method to be the page number as an int, so that the typedPaginator can feed the page number to the Binding appropriately. As well as this, the RetT type must be an array type.

• attrs: the Attr functions used to add attributes to the Binding. These attributes can be retrieved in the Binding.Request, Binding.ResponseWrapper, Binding.ResponseUnwrapped, and Binding.Response methods using the Binding.Attrs method. Each Attr function is passed the Client instance. NewBinding will initially evaluate each of these Attr functions using a null Client. If any Attr functions panic then during this initial evaluation, they will be subsequently evaluated in Binding.Execute where they will be passed the Client that is passed to Binding.Execute.

Please see the example for NewAPI on how to use this method practice.

func NewBindingChain

func NewBindingChain[ResT any, RetT any](request BindingRequestMethod[ResT, RetT]) Binding[ResT, RetT]

NewBindingChain creates a new Binding for an API via a prototype that implements the Binding interface. Unlike the NewBinding constructor, NewBindingChain takes only a BindingRequestMethod (the other methods for Binding have no default implementation) the returned Binding can then have its methods and properties set using the various setters available on the Binding interface.

type BindingExecuteMethod

type BindingExecuteMethod[ResT any, RetT any] func(binding Binding[ResT, RetT], client Client, args ...any) (response RetT, err error)

type BindingParam

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

BindingParam represents a param for a Binding. Binding.Execute uses BindingParam(s) for type-checking the arguments passed into it. To create a BindingParam use the available constructors:

  • Param
  • ReqParam
  • Params

func Param

func Param(name string, val any) BindingParam

Param returns a non-required BindingParam with the given name and default value. The required type for this BindingParam will be found using reflection on this default value.

func Params

func Params(args ...any) []BindingParam

Params constructs an array of BindingParam using the given arguments. The arguments will be treated as groupings of 2-4 values:

  1. "name" (string): the name of the BindingParam.
  2. "val" (any): the default value/type of the BindingParam. If this is reflect.Value or reflect.Type then the type information will be taken from these. If you want a required BindingParam that checks for an interface value then you will have to pass in a reflect.Value/reflect.Type of the pointer to the nil interface. For instance, if you wanted to pass in a required Client instance to Binding.Execute: "reflect.TypeOf((*Client)(nil))". The type of the Client interface will then be correctly stored in the BindingParam. However, if you wanted to pass in a non-required Client instance to Binding.Execute then you would need to pass in a reflect.Value of a pointer to the default value: "reflect.ValueOf(&client)" (where "client" is an instance of Client).
  3. "required" (bool): whether this BindingParam is required. This is optional and will default to false.
  4. "variadic" (bool): whether this BindingParam is variadic. This is also optional, but "required" must also be given, otherwise the "variadic" argument is treated as the "required" argument. Defaults to false. This will also treat "required" as false when given.

If any of these groupings are incomplete/incorrect, then we will ignore that grouping. Each incomplete grouping will be ignored until the next instance of a string argument (i.e. the name of the next BindingParam). Be careful when there is BindingParam for a string/bool parameter. This is because Params might treat the "val" argument in the grouping as either the "name" or the "required" argument.

Note: that Params will not be checked if they follow the rules described in the documentation for BindingParam. These rules are checked when Binding.Params or Binding.SetParamsMethod is called for a Binding, and the appropriate error is cached until Binding.Execute is called.

Example
// Define some types and instance to use in the example...
type A struct {
	A int
	B int
}

// Your interfaces would probably have methods...
type AInterface interface {
}
var aInterfaceInstance AInterface = A{}

params := Params(
	// Required params should come first...
	// "page" is a required parameter of the type: "int". The type is taken from the "val" argument in the grouping.
	"page", 1, true,
	// "a" is a required parameter of the type: "A"
	"a", A{}, true,
	// "*a" is a required parameter of the type: "*A"
	"*a", &A{}, true,
	// "typeof(a)" is a required parameter of the type: "A", with the default value: "nil".
	"typeof(a)", reflect.TypeOf(A{}), true,
	// "valueof(a)" if the required parameter of the type "A", with the default value: "A". The default value is not
	// used for required params.
	"valueof(a)", reflect.ValueOf(A{}), true,
	// "client" is a required parameter of the type "Client". This is how to pass in arguments of interface types.
	"client", reflect.TypeOf((*Client)(nil)), true,

	// "Non-required params should come after required params..."
	"greeting", "hello world!",
	"interfaceDefault", reflect.ValueOf(&aInterfaceInstance),

	// There can only be one variadic parameter that should come last...
	// "variadic" is a variadic parameter of the type: "int?...". The "required" argument in the grouping will
	// always be set to false by Params.
	"variadic", []int{}, true, true,
)

// Print the parameters in a nice comma seperated list
fmt.Printf("(%s)\n", strings.Join(slices.Comprehension(params, func(idx int, value BindingParam, arr []BindingParam) string {
	return value.String()
}), ", "))
Output:

(page: int, a: api.A, *a: *api.A, typeof(a): api.A, valueof(a): api.A, client: [I]api.Client, greeting: string? = "hello world!", interfaceDefault: [I]api.AInterface? = {0 0}, variadic: []int?... = [])

func ReqParam

func ReqParam(name string, val any) BindingParam

ReqParam returns a required BindingParam with the given name and type (reflected from the given value).

func VarParam

func VarParam(name string, val any) BindingParam

VarParam returns a variadic BindingParam with the given name and type (reflected from the given value).

func (BindingParam) String

func (bp BindingParam) String() string

String returns the string representation of the BindingParam in the format:

<name>: ["[I]" if interface]<type>["?" if !required]["..." if variadic][" = <defaultValue>" if !required]

func (BindingParam) Type

func (bp BindingParam) Type() reflect.Type

Type returns the reflect.Type of the BindingParam.

type BindingParamsMethod

type BindingParamsMethod[ResT any, RetT any] func(binding Binding[ResT, RetT]) []BindingParam

type BindingRequestMethod

type BindingRequestMethod[ResT any, RetT any] func(binding Binding[ResT, RetT], args ...any) (request Request)

type BindingResponseMethod

type BindingResponseMethod[ResT any, RetT any] func(binding Binding[ResT, RetT], response ResT, args ...any) RetT

type BindingResponseUnwrappedMethod

type BindingResponseUnwrappedMethod[ResT any, RetT any] func(binding Binding[ResT, RetT], responseWrapper reflect.Value, args ...any) (response ResT, err error)

type BindingResponseWrapperMethod

type BindingResponseWrapperMethod[ResT any, RetT any] func(binding Binding[ResT, RetT], args ...any) (responseWrapper reflect.Value, err error)

type BindingWrapper

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

BindingWrapper wraps a Binding value with its name. This is used within the Schema map so that we don't have to use type parameters everywhere.

func NewWrappedBinding

func NewWrappedBinding[ResT any, RetT any](
	name string,
	request BindingRequestMethod[ResT, RetT],
	wrap BindingResponseWrapperMethod[ResT, RetT],
	unwrap BindingResponseUnwrappedMethod[ResT, RetT],
	response BindingResponseMethod[ResT, RetT],
	params BindingParamsMethod[ResT, RetT],
	paginated bool,
	attrs ...Attr,
) BindingWrapper

NewWrappedBinding calls NewBinding then wraps the returned Binding with WrapBinding.

func WrapBinding

func WrapBinding[ResT any, RetT any](binding Binding[ResT, RetT]) BindingWrapper

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 (BindingWrapper) ArgsFromStrings

func (bw BindingWrapper) ArgsFromStrings(args ...string) (parsedArgs []any, err error)

ArgsFromStrings calls the Binding.ArgsFromStrings method for the underlying Binding in the BindingWrapper.

func (BindingWrapper) Execute

func (bw BindingWrapper) Execute(client Client, args ...any) (val any, err error)

Execute calls the Binding.Execute method for the underlying Binding in the BindingWrapper.

func (BindingWrapper) Name

func (bw BindingWrapper) Name() string

Name returns the name of the underlying Binding.

func (BindingWrapper) Paginated

func (bw BindingWrapper) Paginated() bool

Paginated calls the Binding.Paginated method for the underlying Binding in the BindingWrapper.

func (BindingWrapper) Paginator

func (bw BindingWrapper) Paginator(client Client, waitTime time.Duration, args ...any) (paginator Paginator[any, any], err error)

Paginator returns an un-typed Paginator for the underlying Binding of the BindingWrapper.

func (BindingWrapper) Params

func (bw BindingWrapper) Params() []BindingParam

Params calls the Binding.Params method for the underlying Binding in the BindingWrapper.

func (BindingWrapper) String

func (bw BindingWrapper) String() string

type Client

type Client interface {
	// 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
}

Client is the API client that will execute the Binding.

type GraphQLRequest

type GraphQLRequest struct {
	*graphql.Request
}

GraphQLRequest is a wrapper for graphql.Request that implements the Request interface.

func (GraphQLRequest) Header

func (req GraphQLRequest) Header() *http.Header

type HTTPRequest

type HTTPRequest struct {
	*http.Request
}

HTTPRequest is a wrapper for http.Request that implements the Request interface.

func (HTTPRequest) Header

func (req HTTPRequest) Header() *http.Header

type Lenable

type Lenable interface {
	Len() int
}

Lenable is an interface that provides the Len interface for calculating the length of things.

type Mergeable

type Mergeable interface {
	// Merge merges the given value into the Mergeable instance.
	Merge(similar any) error
	// HasMore returns true if there are more pages to fetch.
	HasMore() bool
}

Mergeable denotes whether a return type can be merged in a Paginator for a Binding. Instances of Mergeable can be used instead of reflect.Slice or reflect.Array types as a return type for a Paginator.

type Paginator

type Paginator[ResT any, RetT any] interface {
	// Continue returns whether the Paginator can continue fetching more pages for the Binding. This will also return true
	// when the Paginator is on the first page.
	Continue() bool
	// Page fetches the current page of results.
	Page() RetT
	// Next fetches the next page from the Binding. The result can be fetched using the Page method.
	Next() error
	// All returns all the return values for the Binding at once.
	All() (RetT, error)
	// Pages fetches the given number of pages from the Binding whilst appending each response slice together.
	Pages(pages int) (RetT, error)
	// Until keeps fetching pages until there are no more pages, or the given predicate function returns false.
	Until(predicate func(paginator Paginator[ResT, RetT], pages RetT) bool) (RetT, error)
}

Paginator can fetch resources from a Binding that is paginated. Use NewPaginator or NewTypedPaginator to create a new one for a given Binding.

func MustPaginate

func MustPaginate(client Client, waitTime time.Duration, binding BindingWrapper, args ...any) (paginator Paginator[any, any])

MustPaginate calls NewPaginator for the given arguments and panics if an error occurs.

func MustTypePaginate

func MustTypePaginate[ResT any, RetT any](client Client, waitTime time.Duration, binding Binding[ResT, RetT], args ...any) (paginator Paginator[ResT, RetT])

MustTypePaginate calls NewTypedPaginator with the given arguments and panics if an error occurs.

func NewPaginator

func NewPaginator(client Client, waitTime time.Duration, binding BindingWrapper, args ...any) (pag Paginator[any, any], err error)

NewPaginator creates an un-typed Paginator for the given BindingWrapper. It creates a Paginator in a similar way as NewTypedPaginator, except the return type of the Paginator is []any. See NewTypedPaginator for more information on Paginator construction.

func NewTypedPaginator

func NewTypedPaginator[ResT any, RetT any](client Client, waitTime time.Duration, binding Binding[ResT, RetT], args ...any) (paginator Paginator[ResT, RetT], err error)

NewTypedPaginator creates a new type aware Paginator using the given Client, wait time.Duration, and arguments for the given Binding. The given Binding's Binding.Paginated method must return true, and the return type (RetT) of the Binding must be a slice-type, otherwise an appropriate error will be returned.

The Paginator requires one of the following sets of BindingParam(s) taken by the given Binding:

  1. ("page",): a singular page argument where each time Paginator.Next is called the page will be incremented
  2. ("after",): a singular after argument where each time Paginator.Next is called the Afterable.After method will be called on the returned response and the returned value will be set as the "after" parameter for the next Binding.Execute. This requires the RetT to implement the Afterable interface.

The sets of BindingParam(s) shown above are given in priority order. This means that a Binding that defines multiple BindingParam(s) that exist within these sets, only the first complete set will be taken.

The args given to NewTypedPaginator should not include the set of BindingParam(s) (listed above), that are going to be used to paginate the binding.

If the given Client also implements RateLimitedClient then the given waitTime argument will be ignored in favour of waiting (or not) until the RateLimit for the given Binding resets. If the RateLimit that is returned by RateLimitedClient.LatestRateLimit is of type ResourceRateLimit, and the Paginator is on the first page. The following parameter arguments will be checked for a limit/count value to see whether there is enough RateLimit.Remaining (in priority order):

  1. "limit"
  2. "count"

type RateLimit

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
}

RateLimit represents a RateLimit for a binding.

type RateLimitType

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
)

type RateLimitedClient

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
	// Log should be implemented for debugging purposes. If you do not require it then you can define it but have it do
	// nothing.
	Log(string)
}

RateLimitedClient is an API Client that has a RateLimit for each Binding it has authority over.

type Request

type Request interface {
	// Header returns a reference to the http.Header for the underlying http.Request that can be modified in any way
	// necessary by Binding.Execute.
	Header() *http.Header
}

Request is the request instance that is constructed by the Binding.Request method.

type Schema

type Schema map[string]BindingWrapper

Schema is a mapping of names to BindingWrapper(s).

Jump to

Keyboard shortcuts

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