Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(go): use functional options pattern #3306

Merged
merged 6 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading