diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java index 788cba5aba3..27018385952 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java @@ -54,6 +54,10 @@ public void processOpts() { supportingFiles.add(new SupportingFile("configuration.mustache", "", "configuration.go")); supportingFiles.add(new SupportingFile("client.mustache", "", "client.go")); + if ("search".equals(client)) { + supportingFiles.add(new SupportingFile("transformation_options.mustache", "", "transformation_options.go")); + } + Helpers.addCommonSupportingFiles(supportingFiles, "../../"); try { diff --git a/templates/go/client.mustache b/templates/go/client.mustache index a8f0692ae21..dcd0b31f691 100644 --- a/templates/go/client.mustache +++ b/templates/go/client.mustache @@ -19,7 +19,8 @@ import ( "github.com/algolia/algoliasearch-client-go/v4/algolia/call" "github.com/algolia/algoliasearch-client-go/v4/algolia/compression" - "github.com/algolia/algoliasearch-client-go/v4/algolia/transport" + {{#isSearchClient}}"github.com/algolia/algoliasearch-client-go/v4/algolia/ingestion" + {{/isSearchClient}}"github.com/algolia/algoliasearch-client-go/v4/algolia/transport" ) // APIClient manages communication with the {{appName}} API v{{version}} @@ -34,8 +35,8 @@ type APIClient struct { } // NewClient creates a new API client with {{#hasRegionalHost}}appID, apiKey and region.{{/hasRegionalHost}}{{^hasRegionalHost}}appID and apiKey.{{/hasRegionalHost}} -func NewClient(appID, apiKey string{{#hasRegionalHost}}, region Region{{/hasRegionalHost}}) (*APIClient, error) { -return NewClientWithConfig({{#lambda.titlecase}}{{#lambda.camelcase}}{{client}}{{/lambda.camelcase}}{{/lambda.titlecase}}Configuration{ +func NewClient(appID, apiKey string{{#hasRegionalHost}}, region Region{{/hasRegionalHost}}{{#isSearchClient}}, opts ...ClientOption{{/isSearchClient}}) (*APIClient, error) { + cfg := {{#lambda.titlecase}}{{#lambda.camelcase}}{{client}}{{/lambda.camelcase}}{{/lambda.titlecase}}Configuration{ Configuration: transport.Configuration{ AppID: appID, ApiKey: apiKey, @@ -44,7 +45,13 @@ return NewClientWithConfig({{#lambda.titlecase}}{{#lambda.camelcase}}{{client}}{ Requester: transport.NewDefaultRequester(nil), },{{#hasRegionalHost}} Region: region,{{/hasRegionalHost}} - }) + } + {{#isSearchClient}} + for _, opt := range opts { + opt(&cfg) + } + {{/isSearchClient}} + return NewClientWithConfig(cfg) } // NewClientWithConfig creates a new API client with the given configuration to fully customize the client behaviour. @@ -83,26 +90,11 @@ func NewClientWithConfig(cfg {{#lambda.titlecase}}{{#lambda.camelcase}}{{client} } {{#isSearchClient}} - if cfg.Transformation != nil && cfg.Transformation.Region != "" { - ingestionConfig := ingestion.IngestionConfiguration{ - Configuration: transport.Configuration{ - AppID: cfg.AppID, - ApiKey: cfg.ApiKey, - }, - Region: cfg.Transformation.Region, + if cfg.TransformationOptions != nil { + if err := apiClient.SetTransformationOptions(*cfg.TransformationOptions); err != nil { + return nil, err } - - if len(cfg.Hosts) > 0 { - ingestionConfig.Hosts = cfg.Hosts - } - - ingestionClient, err := ingestion.NewClientWithConfig(ingestionConfig) - if err != nil { - return nil, err //nolint:wrapcheck - } - - apiClient.ingestionTransporter = ingestionClient - } + } {{/isSearchClient}} return &apiClient, nil @@ -165,12 +157,57 @@ func (c *APIClient) SetClientApiKey(apiKey string) error { if c.cfg == nil { return errors.New("client config is not set") } - + c.cfg.ApiKey = apiKey return nil } +{{#isSearchClient}} +// SetTransformationOptions sets (or replaces) the ingestion transporter used by *WithTransformation helpers. +// The transporter uses Ingestion API defaults (25s timeouts); only fields set in opts override those defaults. +// See https://www.algolia.com/doc/libraries/sdk/methods/ingestion +func (c *APIClient) SetTransformationOptions(opts TransformationOptions) error { + err := opts.validate() + if err != nil { + return err + } + ingestionConfig := ingestion.IngestionConfiguration{ + Configuration: transport.Configuration{ + AppID: c.cfg.AppID, + ApiKey: c.cfg.ApiKey, + }, + Region: opts.Region, + } + if opts.ReadTimeout != 0 { + ingestionConfig.ReadTimeout = opts.ReadTimeout + } + if opts.WriteTimeout != 0 { + ingestionConfig.WriteTimeout = opts.WriteTimeout + } + if opts.ConnectTimeout != 0 { + ingestionConfig.ConnectTimeout = opts.ConnectTimeout + } + if opts.Compression != 0 { + ingestionConfig.Compression = opts.Compression + } + if len(opts.Hosts) > 0 { + ingestionConfig.Hosts = opts.Hosts + } + if opts.DefaultHeader != nil { + ingestionConfig.DefaultHeader = opts.DefaultHeader + } + ingestionClient, err := ingestion.NewClientWithConfig(ingestionConfig) + if err != nil { + return err //nolint:wrapcheck + } + c.ingestionTransporter = ingestionClient + c.cfg.TransformationOptions = &opts + return nil +} + +{{/isSearchClient}} + // callAPI do the request. func (c *APIClient) callAPI(request *http.Request, useReadTransporter bool, requestConfiguration transport.RequestConfiguration) (*http.Response, []byte, error) { callKind := call.Write diff --git a/templates/go/configuration.mustache b/templates/go/configuration.mustache index c301d631451..aa61370a085 100644 --- a/templates/go/configuration.mustache +++ b/templates/go/configuration.mustache @@ -20,22 +20,12 @@ const ( // {{#lambda.titlecase}}{{#lambda.camelcase}}{{client}}{{/lambda.camelcase}}{{/lambda.titlecase}}Configuration stores the configuration of the API client. type {{#lambda.titlecase}}{{#lambda.camelcase}}{{client}}{{/lambda.camelcase}}{{/lambda.titlecase}}Configuration struct { transport.Configuration - + {{#hasRegionalHost}}Region Region{{/hasRegionalHost}} {{#isSearchClient}} - Transformation *TransformationConfiguration + // TransformationOptions configures the ingestion transporter used by *WithTransformation helpers. + // When set, the transporter is eagerly created using Ingestion API defaults. + TransformationOptions *TransformationOptions {{/isSearchClient}} } -{{#isSearchClient}} -type TransformationConfiguration struct { - Region ingestion.Region -} - -// SetTransformationRegion sets the region of the current algolia application to the configuration, this is required to be called if you wish to leverage the transformation pipeline (via the *WithTransformation methods). -func (s *SearchConfiguration) SetTransformationRegion(region ingestion.Region) { - s.Transformation = &TransformationConfiguration{ - Region: region, - } -} -{{/isSearchClient}} \ No newline at end of file diff --git a/templates/go/search_helpers.mustache b/templates/go/search_helpers.mustache index 82b5bc33672..ef0047fb450 100644 --- a/templates/go/search_helpers.mustache +++ b/templates/go/search_helpers.mustache @@ -600,7 +600,7 @@ func (c *APIClient) ChunkedBatch(indexName string, objects []map[string]any, act } /* -ReplaceAllObjectsWithTransformation is similar to the `replaceAllObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. The `region` must have been passed to the client instantiation method. +ReplaceAllObjectsWithTransformation is similar to the `replaceAllObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. TransformationOptions must have been passed to the client constructor or set via SetTransformationOptions. See https://api-clients-automation.netlify.app/docs/custom-helpers/#replaceallobjects for implementation details. @param indexName string - the index name to replace objects into. @@ -611,7 +611,7 @@ See https://api-clients-automation.netlify.app/docs/custom-helpers/#replaceallob */ func (c *APIClient) ReplaceAllObjectsWithTransformation(indexName string, objects []map[string]any, opts ...ReplaceAllObjectsOption) (*ReplaceAllObjectsWithTransformationResponse, error) { if c.ingestionTransporter == nil { - return nil, reportError("`region` must be provided at client instantiation before calling this method.") + return nil, reportError("TransformationOptions must be set in the client config before calling this method. It defaults to the Ingestion API defaults. See https://www.algolia.com/doc/libraries/sdk/methods/ingestion") } tmpIndexName := fmt.Sprintf("%s_tmp_%d", indexName, time.Now().UnixNano()) @@ -789,7 +789,7 @@ func (c *APIClient) IndexExists(indexName string) (bool, error) { } /* -Helper: Similar to the `SaveObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. The `region` must've been passed to the client's config at instantiation. +Helper: Similar to the `SaveObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. TransformationOptions must have been passed to the client constructor or set via SetTransformationOptions. @param indexName string - the index name to save objects into. @param objects []map[string]any - List of objects to save. @@ -799,14 +799,14 @@ Helper: Similar to the `SaveObjects` method but requires a Push connector (https */ func (c *APIClient) SaveObjectsWithTransformation(indexName string, objects []map[string]any, opts ...ChunkedBatchOption) ([]ingestion.WatchResponse, error) { if c.ingestionTransporter == nil { - return nil, reportError("`region` must be provided at client instantiation before calling this method.") + return nil, reportError("TransformationOptions must be set in the client config before calling this method. It defaults to the Ingestion API defaults. See https://www.algolia.com/doc/libraries/sdk/methods/ingestion") } return c.ingestionTransporter.ChunkedPush(indexName, objects, ingestion.Action(ACTION_ADD_OBJECT), nil, toIngestionChunkedBatchOptions(opts)...) //nolint:wrapcheck } /* -Helper: Similar to the `PartialUpdateObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. The `region` must've been passed to the client instantiation method. +Helper: Similar to the `PartialUpdateObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. TransformationOptions must have been passed to the client constructor or set via SetTransformationOptions. @param indexName string - the index name to save objects into. @param objects []map[string]any - List of objects to save. @@ -816,7 +816,7 @@ Helper: Similar to the `PartialUpdateObjects` method but requires a Push connect */ func (c *APIClient) PartialUpdateObjectsWithTransformation(indexName string, objects []map[string]any, opts ...PartialUpdateObjectsOption) ([]ingestion.WatchResponse, error) { if c.ingestionTransporter == nil { - return nil, reportError("`region` must be provided at client instantiation before calling this method.") + return nil, reportError("TransformationOptions must be set in the client config before calling this method. It defaults to the Ingestion API defaults. See https://www.algolia.com/doc/libraries/sdk/methods/ingestion") } conf := config{ diff --git a/templates/go/tests/client/createClient.mustache b/templates/go/tests/client/createClient.mustache index f0ff743823e..382424647c5 100644 --- a/templates/go/tests/client/createClient.mustache +++ b/templates/go/tests/client/createClient.mustache @@ -1,3 +1,6 @@ +{{#hasTransformationOptions}} +transformationOptions := search.TransformationOptions{Region: "{{{transformationRegion}}}"{{#hasTransformationCustomHosts}}, Hosts: []transport.StatefulHost{ {{#transformationCustomHosts}}transport.NewStatefulHost("http", tests.GetLocalhost() + ":{{port}}", call.IsReadWrite),{{/transformationCustomHosts}} }{{/hasTransformationCustomHosts}}} +{{/hasTransformationOptions}} cfg = {{clientPrefix}}.{{clientName}}Configuration{ Configuration: transport.Configuration{ AppID: "{{parametersWithDataTypeMap.appId.value}}", @@ -13,8 +16,10 @@ cfg = {{clientPrefix}}.{{clientName}}Configuration{ {{/gzipEncoding}} },{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region.value}} Region: {{clientPrefix}}.Region("{{parametersWithDataTypeMap.region.value}}"),{{/parametersWithDataTypeMap.region.value}}{{/hasRegionalHost}} - {{#hasTransformationRegion}} - Transformation: &search.TransformationConfiguration{ Region: "{{{transformationRegion}}}" }, - {{/hasTransformationRegion}} } -client, err = {{clientPrefix}}.NewClientWithConfig(cfg) \ No newline at end of file +client, err = {{clientPrefix}}.NewClientWithConfig(cfg) +{{#hasTransformationOptions}} +require.NoError(t, err) +err = client.SetTransformationOptions(transformationOptions) +require.NoError(t, err) +{{/hasTransformationOptions}} \ No newline at end of file diff --git a/templates/go/transformation_options.mustache b/templates/go/transformation_options.mustache new file mode 100644 index 00000000000..73d76afacbb --- /dev/null +++ b/templates/go/transformation_options.mustache @@ -0,0 +1,43 @@ +// {{{generationBanner}}} +package {{packageName}} + +import ( + "errors" + "time" + + "github.com/algolia/algoliasearch-client-go/v4/algolia/compression" + "github.com/algolia/algoliasearch-client-go/v4/algolia/ingestion" + "github.com/algolia/algoliasearch-client-go/v4/algolia/transport" +) + +// TransformationOptions configures the ingestion transporter used by *WithTransformation helpers. +// When passed to NewClient or set via SetTransformationOptions, the transporter is eagerly created +// using Ingestion API defaults (25s timeouts, no compression). Only fields explicitly set here override +// those defaults. The parent search config is NOT forwarded to the ingestion transporter. +// See https://www.algolia.com/doc/libraries/sdk/methods/ingestion +type TransformationOptions struct { + Region ingestion.Region // required + ReadTimeout time.Duration + WriteTimeout time.Duration + ConnectTimeout time.Duration + Compression compression.Compression + Hosts []transport.StatefulHost + DefaultHeader map[string]string +} + +func (o TransformationOptions) validate() error { + if o.Region == "" { + return errors.New("Region is required in TransformationOptions. See https://www.algolia.com/doc/libraries/sdk/methods/ingestion") + } + return nil +} + +// ClientOption configures the search client at construction time. +type ClientOption func(*SearchConfiguration) + +// WithTransformationOptions returns a ClientOption that configures the ingestion transporter. +func WithTransformationOptions(opts TransformationOptions) ClientOption { + return func(cfg *SearchConfiguration) { + cfg.TransformationOptions = &opts + } +} diff --git a/tests/output/go/tests/manual/transformation_options_test.go b/tests/output/go/tests/manual/transformation_options_test.go new file mode 100644 index 00000000000..d5406519808 --- /dev/null +++ b/tests/output/go/tests/manual/transformation_options_test.go @@ -0,0 +1,82 @@ +package manual + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" +) + +func TestTransformationOptionsRegionRequired(t *testing.T) { + client, err := search.NewClient("appID", "apiKey") + require.NoError(t, err) + err = client.SetTransformationOptions(search.TransformationOptions{}) + require.ErrorContains(t, err, "Region is required in TransformationOptions") +} + +func TestTransformationOptionsRegionRequiredAtConstruction(t *testing.T) { + _, err := search.NewClient("appID", "apiKey", + search.WithTransformationOptions(search.TransformationOptions{}), + ) + require.ErrorContains(t, err, "Region is required in TransformationOptions") +} + +func TestTransformationOptionsRegionOnlyHasZeroOverrides(t *testing.T) { + opts := search.TransformationOptions{Region: "us"} + require.Equal(t, "us", string(opts.Region)) + require.Zero(t, opts.ReadTimeout) +} + +func TestTransformationOptionsWithOverrides(t *testing.T) { + opts := search.TransformationOptions{Region: "eu", ReadTimeout: 50 * time.Second} + require.Equal(t, "eu", string(opts.Region)) + require.Equal(t, 50*time.Second, opts.ReadTimeout) +} + +func TestTransformationOptionsWithRegionOnly(t *testing.T) { + client, err := search.NewClient("appID", "apiKey") + require.NoError(t, err) + err = client.SetTransformationOptions(search.TransformationOptions{Region: "us"}) + require.NoError(t, err) +} + +func TestIngestionTransporterNilBeforeSet(t *testing.T) { + client, err := search.NewClient("appID", "apiKey") + require.NoError(t, err) + _, err = client.SaveObjectsWithTransformation("index", []map[string]any{{"objectID": "1"}}) + require.ErrorContains(t, err, "TransformationOptions must be set") +} + +func TestSetTransformationOptionsCreatesTransporter(t *testing.T) { + client, err := search.NewClient("appID", "apiKey") + require.NoError(t, err) + _, err = client.SaveObjectsWithTransformation("index", []map[string]any{{"objectID": "1"}}) + require.ErrorContains(t, err, "TransformationOptions must be set") + require.NoError(t, client.SetTransformationOptions(search.TransformationOptions{Region: "us"})) + _, err = client.SaveObjectsWithTransformation("index", []map[string]any{{"objectID": "1"}}) + require.Error(t, err) + require.NotContains(t, err.Error(), "TransformationOptions must be set") +} + +func TestWithTransformationOptionsFunctionalOption(t *testing.T) { + client, err := search.NewClient("appID", "apiKey", + search.WithTransformationOptions(search.TransformationOptions{ + Region: "us", + ReadTimeout: 50 * time.Second, + }), + ) + require.NoError(t, err) + _ = client +} + +func TestSetTransformationOptionsReplacesTransporter(t *testing.T) { + client, err := search.NewClient("appID", "apiKey", + search.WithTransformationOptions(search.TransformationOptions{Region: "us"}), + ) + require.NoError(t, err) + // Replace with a different region — must not error + err = client.SetTransformationOptions(search.TransformationOptions{Region: "eu"}) + require.NoError(t, err) +}