Skip to content

Commit

Permalink
fix(go): use functional options pattern (#3306)
Browse files Browse the repository at this point in the history
  • Loading branch information
millotp authored Jul 16, 2024
1 parent e5a0081 commit e3d766a
Show file tree
Hide file tree
Showing 37 changed files with 416 additions and 612 deletions.
1 change: 1 addition & 0 deletions clients/algoliasearch-client-go/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ linters:
- canonicalheader
- mnd
- perfsprint
- containedctx

# Deprecated
- execinquery
Expand Down
6 changes: 3 additions & 3 deletions clients/algoliasearch-client-go/algolia/errs/wait_err.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ func NewWaitError(msg string) *WaitError {
}
}

func (e *WaitError) Error() string {
func (e WaitError) Error() string {
return e.msg
}

type WaitKeyUpdateError struct{}

func (e *WaitKeyUpdateError) Error() string {
func (e WaitKeyUpdateError) Error() string {
return "`apiKey` is required when waiting for an `update` operation."
}

type WaitKeyOperationError struct{}

func (e *WaitKeyOperationError) Error() string {
func (e WaitKeyOperationError) Error() string {
return "`operation` must be one of `add`, `update` or `delete`."
}
157 changes: 157 additions & 0 deletions clients/algoliasearch-client-go/algolia/utils/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package utils

import (
"context"
"net/url"
"time"
)

type Options struct {
// -- Request options for API calls
Context context.Context
QueryParams url.Values
HeaderParams map[string]string

// -- ChunkedBatch options
WaitForTasks bool
BatchSize int

// -- Iterable options
MaxRetries int
Timeout func(int) time.Duration
Aggregator func(any, error)
IterableError *IterableError
}

// --------- Request options for API calls ---------

type RequestOption interface {
Apply(*Options)
}

type requestOption func(*Options)

func (r requestOption) Apply(o *Options) {
r(o)
}

func WithContext(ctx context.Context) requestOption {
return requestOption(func(o *Options) {
o.Context = ctx
})
}

func WithHeaderParam(key string, value any) requestOption {
return requestOption(func(o *Options) {
o.HeaderParams[key] = ParameterToString(value)
})
}

func WithQueryParam(key string, value any) requestOption {
return requestOption(func(o *Options) {
o.QueryParams.Set(QueryParameterToString(key), QueryParameterToString(value))
})
}

// --------- ChunkedBatch options ---------

type ChunkedBatchOption interface {
RequestOption
chunkedBatch()
}

type chunkedBatchOption func(*Options)

var (
_ ChunkedBatchOption = (*chunkedBatchOption)(nil)
_ ChunkedBatchOption = (*requestOption)(nil)
)

func (c chunkedBatchOption) Apply(o *Options) {
c(o)
}

func (c chunkedBatchOption) chunkedBatch() {}

func (r requestOption) chunkedBatch() {}

func WithWaitForTasks(waitForTasks bool) chunkedBatchOption {
return chunkedBatchOption(func(o *Options) {
o.WaitForTasks = waitForTasks
})
}

func WithBatchSize(batchSize int) chunkedBatchOption {
return chunkedBatchOption(func(o *Options) {
o.BatchSize = batchSize
})
}

// --------- Iterable options ---------.
type IterableOption interface {
RequestOption
iterable()
}

type iterableOption func(*Options)

var (
_ IterableOption = (*iterableOption)(nil)
_ IterableOption = (*requestOption)(nil)
)

func (i iterableOption) Apply(o *Options) {
i(o)
}

func (r requestOption) iterable() {}

func (i iterableOption) iterable() {}

func WithMaxRetries(maxRetries int) iterableOption {
return iterableOption(func(o *Options) {
o.MaxRetries = maxRetries
})
}

func WithTimeout(timeout func(int) time.Duration) iterableOption {
return iterableOption(func(o *Options) {
o.Timeout = timeout
})
}

func WithAggregator(aggregator func(any, error)) iterableOption {
return iterableOption(func(o *Options) {
o.Aggregator = aggregator
})
}

func WithIterableError(iterableError *IterableError) iterableOption {
return iterableOption(func(o *Options) {
o.IterableError = iterableError
})
}

// --------- Helper to convert options ---------

func ToRequestOptions[T RequestOption](opts []T) []RequestOption {
requestOpts := make([]RequestOption, 0, len(opts))

for _, opt := range opts {
requestOpts = append(requestOpts, opt)
}

return requestOpts
}

func ToIterableOptions(opts []ChunkedBatchOption) []IterableOption {
iterableOpts := make([]IterableOption, 0, len(opts))

for _, opt := range opts {
if opt, ok := opt.(IterableOption); ok {
iterableOpts = append(iterableOpts, opt)
}
}

return iterableOpts
}
85 changes: 63 additions & 22 deletions clients/algoliasearch-client-go/algolia/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package utils

import (
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"time"

"github.com/algolia/algoliasearch-client-go/v4/algolia/errs"
Expand Down Expand Up @@ -65,49 +68,87 @@ func IsNilOrEmpty(i any) bool {
}
}

type IterableError[T any] struct {
Validate func(*T, error) bool
Message func(*T, error) string
type IterableError struct {
Validate func(any, error) bool
Message func(any, error) string
}

func CreateIterable[T any](
execute func(*T, error) (*T, error),
validate func(*T, error) bool,
aggregator func(*T, error),
timeout func() time.Duration,
iterableErr *IterableError[T],
) (*T, error) {
func CreateIterable[T any](execute func(*T, error) (*T, error), validate func(*T, error) bool, opts ...IterableOption) (*T, error) {
options := Options{
MaxRetries: 50,
Timeout: func(_ int) time.Duration {
return 1 * time.Second
},
}

for _, opt := range opts {
opt.Apply(&options)
}

var executor func(*T, error) (*T, error)

retryCount := 0

executor = func(previousResponse *T, previousError error) (*T, error) {
response, responseErr := execute(previousResponse, previousError)

if aggregator != nil {
aggregator(response, responseErr)
retryCount++

if options.Aggregator != nil {
options.Aggregator(response, responseErr)
}

if validate(response, responseErr) {
return response, responseErr
}

if iterableErr != nil && iterableErr.Validate(response, responseErr) {
if iterableErr.Message != nil {
return nil, errs.NewWaitError(iterableErr.Message(response, responseErr))
}

return nil, errs.NewWaitError("an error occurred")
if retryCount >= options.MaxRetries {
return nil, errs.NewWaitError(fmt.Sprintf("The maximum number of retries exceeded. (%d/%d)", retryCount, options.MaxRetries))
}

if timeout == nil {
timeout = func() time.Duration {
return 1 * time.Second
if options.IterableError != nil && options.IterableError.Validate(response, responseErr) {
if options.IterableError.Message != nil {
return nil, errs.NewWaitError(options.IterableError.Message(response, responseErr))
}

return nil, errs.NewWaitError("an error occurred")
}

time.Sleep(timeout())
time.Sleep(options.Timeout(retryCount))

return executor(response, responseErr)
}

return executor(nil, nil)
}

// QueryParameterToString convert any query parameters to string.
func QueryParameterToString(obj any) string {
return strings.ReplaceAll(url.QueryEscape(ParameterToString(obj)), "+", "%20")
}

// ParameterToString convert any parameters to string.
func ParameterToString(obj any) string {
objKind := reflect.TypeOf(obj).Kind()
if objKind == reflect.Slice {
var result []string
sliceValue := reflect.ValueOf(obj)
for i := 0; i < sliceValue.Len(); i++ {
element := sliceValue.Index(i).Interface()
result = append(result, ParameterToString(element))
}
return strings.Join(result, ",")
}

if t, ok := obj.(time.Time); ok {
return t.Format(time.RFC3339)
}

if objKind == reflect.Struct {
if actualObj, ok := obj.(interface{ GetActualInstance() any }); ok {
return ParameterToString(actualObj.GetActualInstance())
}
}

return fmt.Sprintf("%v", obj)
}
2 changes: 1 addition & 1 deletion playground/go/ingestion.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func testIngestion(appID, apiKey string) int {
// another example to generate payload for a request.
createAuthenticationResponse, err := ingestionClient.CreateAuthentication(ingestionClient.NewApiCreateAuthenticationRequest(
&ingestion.AuthenticationCreate{
Type: ingestion.AUTHENTICATIONTYPE_BASIC,
Type: ingestion.AUTHENTICATION_TYPE_BASIC,
Name: fmt.Sprintf("my-authentication-%d", time.Now().Unix()),
Input: ingestion.AuthInput{
AuthBasic: &ingestion.AuthBasic{
Expand Down
2 changes: 1 addition & 1 deletion playground/go/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func testInsights(appID, apiKey string) int {

events := insights.NewInsightsEvents([]insights.EventsItems{
*insights.ClickedObjectIDsAsEventsItems(insights.NewClickedObjectIDs("myEvent",
insights.CLICKEVENT_CLICK,
insights.CLICK_EVENT_CLICK,
"test_index",
[]string{"myObjectID"},
"myToken",
Expand Down
4 changes: 3 additions & 1 deletion playground/go/personalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/algolia/algoliasearch-client-go/v4/algolia/personalization"
"github.com/algolia/algoliasearch-client-go/v4/algolia/utils"
)

func testPersonalization(appID, apiKey string) int {
Expand All @@ -17,8 +18,9 @@ func testPersonalization(appID, apiKey string) int {
defer cancel()

// it will fail expectedly because of the very short timeout to showcase the context usage.
deleteUserProfileResponse, err := personalizationClient.DeleteUserProfileWithContext(ctx,
deleteUserProfileResponse, err := personalizationClient.DeleteUserProfile(
personalizationClient.NewApiDeleteUserProfileRequest("userToken"),
utils.WithContext(ctx),
)
if err != nil {
fmt.Printf("request error with DeleteUserProfile: %v\n", err)
Expand Down
7 changes: 3 additions & 4 deletions playground/go/recommend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/algolia/algoliasearch-client-go/v4/algolia/recommend"
"github.com/algolia/algoliasearch-client-go/v4/algolia/utils"
)

func testRecommend(appID, apiKey string) int {
Expand All @@ -22,11 +21,11 @@ func testRecommend(appID, apiKey string) int {
params := &recommend.GetRecommendationsParams{
Requests: []recommend.RecommendationsRequest{
{
RecommendationsQuery: &recommend.RecommendationsQuery{
Model: recommend.RECOMMENDATIONMODELS_BOUGHT_TOGETHER,
BoughtTogetherQuery: &recommend.BoughtTogetherQuery{
Model: recommend.FBT_MODEL_BOUGHT_TOGETHER,
ObjectID: "test_query",
IndexName: "test_index",
Threshold: utils.PtrInt32(0),
Threshold: 0,
},
},
},
Expand Down
Loading

0 comments on commit e3d766a

Please sign in to comment.