Skip to content

Commit e3d766a

Browse files
authored
fix(go): use functional options pattern (#3306)
1 parent e5a0081 commit e3d766a

File tree

37 files changed

+416
-612
lines changed

37 files changed

+416
-612
lines changed

clients/algoliasearch-client-go/.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ linters:
5757
- canonicalheader
5858
- mnd
5959
- perfsprint
60+
- containedctx
6061

6162
# Deprecated
6263
- execinquery

clients/algoliasearch-client-go/algolia/errs/wait_err.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ func NewWaitError(msg string) *WaitError {
1010
}
1111
}
1212

13-
func (e *WaitError) Error() string {
13+
func (e WaitError) Error() string {
1414
return e.msg
1515
}
1616

1717
type WaitKeyUpdateError struct{}
1818

19-
func (e *WaitKeyUpdateError) Error() string {
19+
func (e WaitKeyUpdateError) Error() string {
2020
return "`apiKey` is required when waiting for an `update` operation."
2121
}
2222

2323
type WaitKeyOperationError struct{}
2424

25-
func (e *WaitKeyOperationError) Error() string {
25+
func (e WaitKeyOperationError) Error() string {
2626
return "`operation` must be one of `add`, `update` or `delete`."
2727
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"net/url"
6+
"time"
7+
)
8+
9+
type Options struct {
10+
// -- Request options for API calls
11+
Context context.Context
12+
QueryParams url.Values
13+
HeaderParams map[string]string
14+
15+
// -- ChunkedBatch options
16+
WaitForTasks bool
17+
BatchSize int
18+
19+
// -- Iterable options
20+
MaxRetries int
21+
Timeout func(int) time.Duration
22+
Aggregator func(any, error)
23+
IterableError *IterableError
24+
}
25+
26+
// --------- Request options for API calls ---------
27+
28+
type RequestOption interface {
29+
Apply(*Options)
30+
}
31+
32+
type requestOption func(*Options)
33+
34+
func (r requestOption) Apply(o *Options) {
35+
r(o)
36+
}
37+
38+
func WithContext(ctx context.Context) requestOption {
39+
return requestOption(func(o *Options) {
40+
o.Context = ctx
41+
})
42+
}
43+
44+
func WithHeaderParam(key string, value any) requestOption {
45+
return requestOption(func(o *Options) {
46+
o.HeaderParams[key] = ParameterToString(value)
47+
})
48+
}
49+
50+
func WithQueryParam(key string, value any) requestOption {
51+
return requestOption(func(o *Options) {
52+
o.QueryParams.Set(QueryParameterToString(key), QueryParameterToString(value))
53+
})
54+
}
55+
56+
// --------- ChunkedBatch options ---------
57+
58+
type ChunkedBatchOption interface {
59+
RequestOption
60+
chunkedBatch()
61+
}
62+
63+
type chunkedBatchOption func(*Options)
64+
65+
var (
66+
_ ChunkedBatchOption = (*chunkedBatchOption)(nil)
67+
_ ChunkedBatchOption = (*requestOption)(nil)
68+
)
69+
70+
func (c chunkedBatchOption) Apply(o *Options) {
71+
c(o)
72+
}
73+
74+
func (c chunkedBatchOption) chunkedBatch() {}
75+
76+
func (r requestOption) chunkedBatch() {}
77+
78+
func WithWaitForTasks(waitForTasks bool) chunkedBatchOption {
79+
return chunkedBatchOption(func(o *Options) {
80+
o.WaitForTasks = waitForTasks
81+
})
82+
}
83+
84+
func WithBatchSize(batchSize int) chunkedBatchOption {
85+
return chunkedBatchOption(func(o *Options) {
86+
o.BatchSize = batchSize
87+
})
88+
}
89+
90+
// --------- Iterable options ---------.
91+
type IterableOption interface {
92+
RequestOption
93+
iterable()
94+
}
95+
96+
type iterableOption func(*Options)
97+
98+
var (
99+
_ IterableOption = (*iterableOption)(nil)
100+
_ IterableOption = (*requestOption)(nil)
101+
)
102+
103+
func (i iterableOption) Apply(o *Options) {
104+
i(o)
105+
}
106+
107+
func (r requestOption) iterable() {}
108+
109+
func (i iterableOption) iterable() {}
110+
111+
func WithMaxRetries(maxRetries int) iterableOption {
112+
return iterableOption(func(o *Options) {
113+
o.MaxRetries = maxRetries
114+
})
115+
}
116+
117+
func WithTimeout(timeout func(int) time.Duration) iterableOption {
118+
return iterableOption(func(o *Options) {
119+
o.Timeout = timeout
120+
})
121+
}
122+
123+
func WithAggregator(aggregator func(any, error)) iterableOption {
124+
return iterableOption(func(o *Options) {
125+
o.Aggregator = aggregator
126+
})
127+
}
128+
129+
func WithIterableError(iterableError *IterableError) iterableOption {
130+
return iterableOption(func(o *Options) {
131+
o.IterableError = iterableError
132+
})
133+
}
134+
135+
// --------- Helper to convert options ---------
136+
137+
func ToRequestOptions[T RequestOption](opts []T) []RequestOption {
138+
requestOpts := make([]RequestOption, 0, len(opts))
139+
140+
for _, opt := range opts {
141+
requestOpts = append(requestOpts, opt)
142+
}
143+
144+
return requestOpts
145+
}
146+
147+
func ToIterableOptions(opts []ChunkedBatchOption) []IterableOption {
148+
iterableOpts := make([]IterableOption, 0, len(opts))
149+
150+
for _, opt := range opts {
151+
if opt, ok := opt.(IterableOption); ok {
152+
iterableOpts = append(iterableOpts, opt)
153+
}
154+
}
155+
156+
return iterableOpts
157+
}

clients/algoliasearch-client-go/algolia/utils/utils.go

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package utils
33

44
import (
55
"encoding/json"
6+
"fmt"
7+
"net/url"
68
"reflect"
9+
"strings"
710
"time"
811

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

68-
type IterableError[T any] struct {
69-
Validate func(*T, error) bool
70-
Message func(*T, error) string
71+
type IterableError struct {
72+
Validate func(any, error) bool
73+
Message func(any, error) string
7174
}
7275

73-
func CreateIterable[T any](
74-
execute func(*T, error) (*T, error),
75-
validate func(*T, error) bool,
76-
aggregator func(*T, error),
77-
timeout func() time.Duration,
78-
iterableErr *IterableError[T],
79-
) (*T, error) {
76+
func CreateIterable[T any](execute func(*T, error) (*T, error), validate func(*T, error) bool, opts ...IterableOption) (*T, error) {
77+
options := Options{
78+
MaxRetries: 50,
79+
Timeout: func(_ int) time.Duration {
80+
return 1 * time.Second
81+
},
82+
}
83+
84+
for _, opt := range opts {
85+
opt.Apply(&options)
86+
}
87+
8088
var executor func(*T, error) (*T, error)
8189

90+
retryCount := 0
91+
8292
executor = func(previousResponse *T, previousError error) (*T, error) {
8393
response, responseErr := execute(previousResponse, previousError)
8494

85-
if aggregator != nil {
86-
aggregator(response, responseErr)
95+
retryCount++
96+
97+
if options.Aggregator != nil {
98+
options.Aggregator(response, responseErr)
8799
}
88100

89101
if validate(response, responseErr) {
90102
return response, responseErr
91103
}
92104

93-
if iterableErr != nil && iterableErr.Validate(response, responseErr) {
94-
if iterableErr.Message != nil {
95-
return nil, errs.NewWaitError(iterableErr.Message(response, responseErr))
96-
}
97-
98-
return nil, errs.NewWaitError("an error occurred")
105+
if retryCount >= options.MaxRetries {
106+
return nil, errs.NewWaitError(fmt.Sprintf("The maximum number of retries exceeded. (%d/%d)", retryCount, options.MaxRetries))
99107
}
100108

101-
if timeout == nil {
102-
timeout = func() time.Duration {
103-
return 1 * time.Second
109+
if options.IterableError != nil && options.IterableError.Validate(response, responseErr) {
110+
if options.IterableError.Message != nil {
111+
return nil, errs.NewWaitError(options.IterableError.Message(response, responseErr))
104112
}
113+
114+
return nil, errs.NewWaitError("an error occurred")
105115
}
106116

107-
time.Sleep(timeout())
117+
time.Sleep(options.Timeout(retryCount))
108118

109119
return executor(response, responseErr)
110120
}
111121

112122
return executor(nil, nil)
113123
}
124+
125+
// QueryParameterToString convert any query parameters to string.
126+
func QueryParameterToString(obj any) string {
127+
return strings.ReplaceAll(url.QueryEscape(ParameterToString(obj)), "+", "%20")
128+
}
129+
130+
// ParameterToString convert any parameters to string.
131+
func ParameterToString(obj any) string {
132+
objKind := reflect.TypeOf(obj).Kind()
133+
if objKind == reflect.Slice {
134+
var result []string
135+
sliceValue := reflect.ValueOf(obj)
136+
for i := 0; i < sliceValue.Len(); i++ {
137+
element := sliceValue.Index(i).Interface()
138+
result = append(result, ParameterToString(element))
139+
}
140+
return strings.Join(result, ",")
141+
}
142+
143+
if t, ok := obj.(time.Time); ok {
144+
return t.Format(time.RFC3339)
145+
}
146+
147+
if objKind == reflect.Struct {
148+
if actualObj, ok := obj.(interface{ GetActualInstance() any }); ok {
149+
return ParameterToString(actualObj.GetActualInstance())
150+
}
151+
}
152+
153+
return fmt.Sprintf("%v", obj)
154+
}

playground/go/ingestion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func testIngestion(appID, apiKey string) int {
1616
// another example to generate payload for a request.
1717
createAuthenticationResponse, err := ingestionClient.CreateAuthentication(ingestionClient.NewApiCreateAuthenticationRequest(
1818
&ingestion.AuthenticationCreate{
19-
Type: ingestion.AUTHENTICATIONTYPE_BASIC,
19+
Type: ingestion.AUTHENTICATION_TYPE_BASIC,
2020
Name: fmt.Sprintf("my-authentication-%d", time.Now().Unix()),
2121
Input: ingestion.AuthInput{
2222
AuthBasic: &ingestion.AuthBasic{

playground/go/insights.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func testInsights(appID, apiKey string) int {
1414

1515
events := insights.NewInsightsEvents([]insights.EventsItems{
1616
*insights.ClickedObjectIDsAsEventsItems(insights.NewClickedObjectIDs("myEvent",
17-
insights.CLICKEVENT_CLICK,
17+
insights.CLICK_EVENT_CLICK,
1818
"test_index",
1919
[]string{"myObjectID"},
2020
"myToken",

playground/go/personalization.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/algolia/algoliasearch-client-go/v4/algolia/personalization"
9+
"github.com/algolia/algoliasearch-client-go/v4/algolia/utils"
910
)
1011

1112
func testPersonalization(appID, apiKey string) int {
@@ -17,8 +18,9 @@ func testPersonalization(appID, apiKey string) int {
1718
defer cancel()
1819

1920
// it will fail expectedly because of the very short timeout to showcase the context usage.
20-
deleteUserProfileResponse, err := personalizationClient.DeleteUserProfileWithContext(ctx,
21+
deleteUserProfileResponse, err := personalizationClient.DeleteUserProfile(
2122
personalizationClient.NewApiDeleteUserProfileRequest("userToken"),
23+
utils.WithContext(ctx),
2224
)
2325
if err != nil {
2426
fmt.Printf("request error with DeleteUserProfile: %v\n", err)

playground/go/recommend.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55

66
"github.com/algolia/algoliasearch-client-go/v4/algolia/recommend"
7-
"github.com/algolia/algoliasearch-client-go/v4/algolia/utils"
87
)
98

109
func testRecommend(appID, apiKey string) int {
@@ -22,11 +21,11 @@ func testRecommend(appID, apiKey string) int {
2221
params := &recommend.GetRecommendationsParams{
2322
Requests: []recommend.RecommendationsRequest{
2423
{
25-
RecommendationsQuery: &recommend.RecommendationsQuery{
26-
Model: recommend.RECOMMENDATIONMODELS_BOUGHT_TOGETHER,
24+
BoughtTogetherQuery: &recommend.BoughtTogetherQuery{
25+
Model: recommend.FBT_MODEL_BOUGHT_TOGETHER,
2726
ObjectID: "test_query",
2827
IndexName: "test_index",
29-
Threshold: utils.PtrInt32(0),
28+
Threshold: 0,
3029
},
3130
},
3231
},

0 commit comments

Comments
 (0)