diff --git a/clients/algoliasearch-client-go/algolia/internal/errs/wait_err.go b/clients/algoliasearch-client-go/algolia/internal/errs/wait_err.go new file mode 100644 index 0000000000..47843e0605 --- /dev/null +++ b/clients/algoliasearch-client-go/algolia/internal/errs/wait_err.go @@ -0,0 +1,19 @@ +package errs + +type WaitError struct{} + +func (e *WaitError) Error() string { + return "wait error" +} + +type WaitKeyUpdateError struct{} + +func (e *WaitKeyUpdateError) Error() string { + return "`apiKey` is required when waiting for an `update` operation." +} + +type WaitKeyOperationError struct{} + +func (e *WaitKeyOperationError) Error() string { + return "`operation` must be one of `add`, `update` or `delete`." +} diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java index 98299208cb..516bd134d6 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java @@ -20,6 +20,7 @@ public String getName() { public void processOpts() { String client = (String) additionalProperties.get("client"); additionalProperties.put("enumClassPrefix", true); + additionalProperties.put("isSearchClient", client.equals("search")); String outputFolder = "algolia" + File.separator + client; setOutputDir(getOutputDir() + File.separator + outputFolder); diff --git a/playground/go/go.mod b/playground/go/go.mod index 6baba832af..e5f76de96d 100644 --- a/playground/go/go.mod +++ b/playground/go/go.mod @@ -1,6 +1,6 @@ module playground -go 1.19 +go 1.21 replace github.com/algolia/algoliasearch-client-go/v4 v4.0.0 => ../../clients/algoliasearch-client-go diff --git a/playground/go/search.go b/playground/go/search.go index db41719a12..fb544fe6d6 100644 --- a/playground/go/search.go +++ b/playground/go/search.go @@ -11,13 +11,93 @@ func testSearch(appID, apiKey string) int { searchClient := search.NewClient(appID, apiKey) searchParams := search.SearchParamsStringAsSearchParams(search.NewSearchParamsString(search.WithSearchParamsStringParams("query=jeans&hitsPerPage=2"))) - searchResponse, err := searchClient.SearchSingleIndex(searchClient.NewApiSearchSingleIndexRequest(indexName).WithSearchParams(&searchParams)) + _, err := searchClient.SearchSingleIndex(searchClient.NewApiSearchSingleIndexRequest(indexName).WithSearchParams(&searchParams)) if err != nil { fmt.Printf("request error with SearchSingleIndex: %v\n", err) return 1 } - printResponse(searchResponse) + apiKeyStruct := search.NewApiKey([]search.Acl{"search"}) + + addApiKeyResponse, err := searchClient.AddApiKey(searchClient.NewApiAddApiKeyRequest(apiKeyStruct)) + if err != nil { + panic(err) + } + + taskResponse, err := searchClient.WaitForApiKey( + addApiKeyResponse.Key, + apiKeyStruct, + "add", + nil, + nil, + nil, + ) + if err != nil { + panic(err) + } + + printResponse(taskResponse) + + apiKeyStruct.SetAcl([]search.Acl{"search", "addObject"}) + + _, err = searchClient.UpdateApiKey(searchClient.NewApiUpdateApiKeyRequest(addApiKeyResponse.Key, apiKeyStruct)) + if err != nil { + panic(err) + } + + taskResponse, err = searchClient.WaitForApiKey( + addApiKeyResponse.Key, + apiKeyStruct, + "update", + nil, + nil, + nil, + ) + if err != nil { + panic(err) + } + + printResponse(taskResponse) + + apiKeyStruct.SetAcl([]search.Acl{"search", "addObject"}) + + _, err = searchClient.UpdateApiKey(searchClient.NewApiUpdateApiKeyRequest(addApiKeyResponse.Key, apiKeyStruct)) + if err != nil { + panic(err) + } + + taskResponse, err = searchClient.WaitForApiKey( + addApiKeyResponse.Key, + apiKeyStruct, + "update", + nil, + nil, + nil, + ) + if err != nil { + panic(err) + } + + printResponse(taskResponse) + + _, err = searchClient.DeleteApiKey(searchClient.NewApiDeleteApiKeyRequest(addApiKeyResponse.Key)) + if err != nil { + panic(err) + } + + taskResponse, err = searchClient.WaitForApiKey( + addApiKeyResponse.Key, + apiKeyStruct, + "delete", + nil, + nil, + nil, + ) + if err != nil { + panic(err) + } + + printResponse(taskResponse) return 0 } diff --git a/templates/go/api.mustache b/templates/go/api.mustache index 6cc641b62a..dc59242997 100644 --- a/templates/go/api.mustache +++ b/templates/go/api.mustache @@ -9,9 +9,18 @@ import ( "io" "net/http" "net/url" + {{#isSearchClient}} + "slices" + {{/isSearchClient}} "strings" + {{#isSearchClient}} + "time" + {{/isSearchClient}} "github.com/algolia/algoliasearch-client-go/v4/algolia/call" + {{#isSearchClient}} + "github.com/algolia/algoliasearch-client-go/v4/algolia/internal/errs" + {{/isSearchClient}} ) type Option struct { @@ -336,3 +345,268 @@ func (c *APIClient) {{nickname}}WithContext(ctx context.Context, {{#hasParams}}r {{/operation}} {{/operations}} + +{{#isSearchClient}} +/* +WaitForTask waits for a task to be published. +Wraps WaitForTaskWithContext with context.Background(). +It returns the task response if the operation was successful. +It returns an error if the operation failed. + +The maxRetries parameter is the maximum number of retries. +The initialDelay parameter is the initial delay between each retry. +The maxDelay parameter is the maximum delay between each retry. + + @param indexName string - Index name. + @param taskID int64 - Task ID. + @param maxRetries *int - Maximum number of retries. + @param initialDelay *time.Duration - Initial delay between retries. + @param maxDelay *time.Duration - Maximum delay between retries. + @param opts ...Option - Optional parameters for the request. + @return *GetTaskResponse - Task response. + @return error - Error if any. +*/ +func (c *APIClient) WaitForTask( + indexName string, + taskID int64, + maxRetries *int, + initialDelay *time.Duration, + maxDelay *time.Duration, + opts ...Option, +) (*GetTaskResponse, error) { + return c.WaitForTaskWithContext( + context.Background(), + indexName, + taskID, + maxRetries, + initialDelay, + maxDelay, + opts..., + ) +} + +/* +WaitForTaskWithContext waits for a task to be published. +It returns the task response if the operation was successful. +It returns an error if the operation failed. + +The maxRetries parameter is the maximum number of retries. +The initialDelay parameter is the initial delay between each retry. +The maxDelay parameter is the maximum delay between each retry. + + @param ctx context.Context - The context that will be drilled down to the actual request. + @param indexName string - Index name. + @param taskID int64 - Task ID. + @param maxRetries *int - Maximum number of retries. + @param initialDelay *time.Duration - Initial delay between retries. + @param maxDelay *time.Duration - Maximum delay between retries. + @param opts ...Option - Optional parameters for the request. + @return *GetTaskResponse - Task response. + @return error - Error if any. +*/ +func (c *APIClient) WaitForTaskWithContext( + ctx context.Context, + indexName string, + taskID int64, + maxRetries *int, + initialDelay *time.Duration, + maxDelay *time.Duration, + opts ...Option, +) (*GetTaskResponse, error) { + return RetryUntil( + func() (*GetTaskResponse, error) { + return c.GetTaskWithContext(ctx, c.NewApiGetTaskRequest(indexName, taskID), opts...) + }, + func(response *GetTaskResponse, err error) bool { + return response.Status == TASKSTATUS_PUBLISHED + }, + maxRetries, + initialDelay, + maxDelay, + ) +} + +/* +WaitForApiKey waits for an API key to be created, deleted or updated. +Wraps WaitForApiKeyWithContext with context.Background(). +It returns the API key response if the operation was successful. +It returns an error if the operation failed. + +The operation can be one of the following: + - "add": wait for the API key to be created + - "delete": wait for the API key to be deleted + - "update": wait for the API key to be updated + +The maxRetries parameter is the maximum number of retries. +The initialDelay parameter is the initial delay between each retry. +The maxDelay parameter is the maximum delay between each retry. + +If the operation is "update", the apiKey parameter must be set. +If the operation is "delete" or "add", the apiKey parameter is not used. + + @param key string - API key. + @param apiKey *ApiKey - API key structure - required for update operation. + @param operation string - Operation type - add, delete or update. + @param maxRetries *int - Maximum number of retries. + @param initialDelay *time.Duration - Initial delay between retries. + @param maxDelay *time.Duration - Maximum delay between retries. + @param opts ...Option - Optional parameters for the request. + @return *GetApiKeyResponse - API key response. + @return error - Error if any. +*/ +func (c *APIClient) WaitForApiKey( + key string, + apiKey *ApiKey, + operation string, + maxRetries *int, + initialDelay *time.Duration, + maxDelay *time.Duration, + opts ...Option, +) (*GetApiKeyResponse, error) { + return c.WaitForApiKeyWithContext( + context.Background(), + key, + apiKey, + operation, + maxRetries, + initialDelay, + maxDelay, + opts..., + ) +} + +/* +WaitForApiKeyWithContext waits for an API key to be created, deleted or updated. +It returns the API key response if the operation was successful. +It returns an error if the operation failed. + +The operation can be one of the following: + - "add": wait for the API key to be created + - "delete": wait for the API key to be deleted + - "update": wait for the API key to be updated + +The maxRetries parameter is the maximum number of retries. +The initialDelay parameter is the initial delay between each retry. +The maxDelay parameter is the maximum delay between each retry. + +If the operation is "update", the apiKey parameter must be set. +If the operation is "delete" or "add", the apiKey parameter is not used. + + @param ctx context.Context - The context that will be drilled down to the actual request. + @param key string - API key. + @param apiKey *ApiKey - API key structure - required for update operation. + @param operation string - Operation type - add, delete or update. + @param maxRetries *int - Maximum number of retries. + @param initialDelay *time.Duration - Initial delay between retries. + @param maxDelay *time.Duration - Maximum delay between retries. + @param opts ...Option - Optional parameters for the request. + @return *GetApiKeyResponse - API key response. + @return error - Error if any. +*/ +func (c *APIClient) WaitForApiKeyWithContext( + ctx context.Context, + key string, + apiKey *ApiKey, + operation string, + maxRetries *int, + initialDelay *time.Duration, + maxDelay *time.Duration, + opts ...Option, +) (*GetApiKeyResponse, error) { + if operation != "add" && operation != "delete" && operation != "update" { + return nil, &errs.WaitKeyOperationError{} + } + + if operation == "update" { + if apiKey == nil { + return nil, &errs.WaitKeyUpdateError{} + } + + return RetryUntil( + func() (*GetApiKeyResponse, error) { + return c.GetApiKeyWithContext(ctx, c.NewApiGetApiKeyRequest(key), opts...) + }, + func(response *GetApiKeyResponse, err error) bool { + if err != nil || response == nil { + return false + } + + if apiKey.GetDescription() != response.GetDescription() { + return false + } + + if apiKey.GetQueryParameters() != response.GetQueryParameters() { + return false + } + + if apiKey.GetMaxHitsPerQuery() != response.GetMaxHitsPerQuery() { + return false + } + + if apiKey.GetMaxQueriesPerIPPerHour() != response.GetMaxQueriesPerIPPerHour() { + return false + } + + if apiKey.GetValidity() != response.GetValidity() { + return false + } + + if apiKey.GetValidity() != response.GetValidity() { + return false + } + + slices.Sort(apiKey.Acl) + slices.Sort(response.Acl) + + if slices.Equal(apiKey.Acl, response.Acl) == false { + return false + } + + slices.Sort(apiKey.Indexes) + slices.Sort(response.Indexes) + + if slices.Equal(apiKey.Indexes, response.Indexes) == false { + return false + } + + slices.Sort(apiKey.Referers) + slices.Sort(response.Referers) + + if slices.Equal(apiKey.Referers, response.Referers) == false { + return false + } + + return true + }, + maxRetries, + initialDelay, + maxDelay, + ) + } + + return RetryUntil( + func() (*GetApiKeyResponse, error) { + return c.GetApiKeyWithContext(ctx, c.NewApiGetApiKeyRequest(key), opts...) + }, + func(response *GetApiKeyResponse, err error) bool { + switch operation { + case "add": + return err == nil && response != nil && response.CreatedAt > 0 + case "delete": + if _, ok := err.(*APIError); ok { + apiErr := err.(*APIError) + + return apiErr.Status == 404 + } + + return false + } + return false + }, + maxRetries, + initialDelay, + maxDelay, + ) +} + +{{/isSearchClient}} diff --git a/templates/go/utils.mustache b/templates/go/utils.mustache index a36649d007..8d2980e636 100644 --- a/templates/go/utils.mustache +++ b/templates/go/utils.mustache @@ -5,6 +5,8 @@ import ( "encoding/json" "reflect" "time" + + "github.com/algolia/algoliasearch-client-go/v4/algolia/internal/errs" ) // PtrBool is a helper routine that returns a pointer to given boolean value. @@ -333,3 +335,42 @@ func isNilorEmpty(i any) bool { return reflect.ValueOf(i).IsZero() } } + +func RetryUntil[T any]( + retry func() (*T, error), + until func(*T, error) bool, + maxRetries *int, + initialDelay *time.Duration, + maxDelay *time.Duration, +) (*T, error) { + if maxRetries == nil { + maxRetries = new(int) + *maxRetries = 50 + } + + if initialDelay == nil { + initialDelay = new(time.Duration) + *initialDelay = 200 * time.Millisecond + } + + if maxDelay == nil { + maxDelay = new(time.Duration) + *maxDelay = 5 * time.Second + } + + for i := 0; i < *maxRetries; i++ { + res, err := retry() + + if ok := until(res, err); ok { + return res, nil + } + + time.Sleep(*initialDelay) + *initialDelay *= 2 + if *initialDelay > *maxDelay { + *initialDelay = *maxDelay + } + } + + return nil, &errs.WaitError{} +} \ No newline at end of file