diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart index 9cc1adc0a02..c27b72414e2 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/algolia_client_core.dart @@ -10,6 +10,7 @@ export 'src/api_client.dart'; export 'src/config/agent_segment.dart'; export 'src/config/client_options.dart'; export 'src/config/host.dart'; +export 'src/config/transformation_options.dart'; export 'src/transport/api_request.dart'; export 'src/transport/request_options.dart'; export 'src/transport/requester.dart'; diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart index 93489f61927..8b11d899262 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/client_options.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'package:algolia_client_core/src/config/agent_segment.dart'; import 'package:algolia_client_core/src/config/host.dart'; +import 'package:algolia_client_core/src/config/transformation_options.dart'; import 'package:algolia_client_core/src/transport/requester.dart'; import 'package:dio/dio.dart'; @@ -42,6 +43,9 @@ final class ClientOptions { /// Set to 'gzip' to enable gzip compression for POST/PUT requests. final String? compression; + /// Options for the ingestion transporter used by `*WithTransformation` helpers on [SearchClient]. + final TransformationOptions? transformationOptions; + /// Constructs a [ClientOptions] instance with the provided parameters. const ClientOptions({ this.connectTimeout = const Duration(seconds: 2), @@ -55,6 +59,7 @@ final class ClientOptions { this.interceptors, this.httpClientAdapter, this.compression, + this.transformationOptions, }); @override @@ -72,7 +77,8 @@ final class ClientOptions { requester == other.requester && interceptors == other.interceptors && httpClientAdapter == other.httpClientAdapter && - compression == other.compression; + compression == other.compression && + transformationOptions == other.transformationOptions; @override int get hashCode => @@ -86,10 +92,11 @@ final class ClientOptions { requester.hashCode ^ interceptors.hashCode ^ httpClientAdapter.hashCode ^ - compression.hashCode; + compression.hashCode ^ + transformationOptions.hashCode; @override String toString() { - return 'ClientOptions{hosts: $hosts, connectTimeout: $connectTimeout, writeTimeout: $writeTimeout, readTimeout: $readTimeout, headers: $headers, agentSegments: $agentSegments, logger: $logger, requester: $requester, interceptors: $interceptors, httpClientAdapter: $httpClientAdapter, compression: $compression}'; + return 'ClientOptions{hosts: $hosts, connectTimeout: $connectTimeout, writeTimeout: $writeTimeout, readTimeout: $readTimeout, headers: $headers, agentSegments: $agentSegments, logger: $logger, requester: $requester, interceptors: $interceptors, httpClientAdapter: $httpClientAdapter, compression: $compression, transformationOptions: $transformationOptions}'; } } diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart new file mode 100644 index 00000000000..8025949444c --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/config/transformation_options.dart @@ -0,0 +1,41 @@ +import 'package:algolia_client_core/src/config/client_options.dart'; + +/// Options for the ingestion transporter used by `*WithTransformation` helpers. +final class TransformationOptions { + /// The Algolia region for the Ingestion API (e.g. `'us'` or `'eu'`). Required. + final String region; + + /// Optional overrides for the ingestion transporter's [ClientOptions]. + /// Only the fields you set here replace the Ingestion API defaults (25 s timeouts). + /// Do not set [ClientOptions.transformationOptions] here — it is ignored. + final ClientOptions? ingestionClientOptions; + + /// Constructs a [TransformationOptions] instance. + TransformationOptions({ + required this.region, + this.ingestionClientOptions, + }) { + if (region.isEmpty) { + throw ArgumentError( + 'region is required in transformationOptions.' + ' See https://www.algolia.com/doc/libraries/sdk/methods/ingestion', + ); + } + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TransformationOptions && + runtimeType == other.runtimeType && + region == other.region && + ingestionClientOptions == other.ingestionClientOptions; + + @override + int get hashCode => region.hashCode ^ ingestionClientOptions.hashCode; + + @override + String toString() { + return 'TransformationOptions{region: $region, ingestionClientOptions: $ingestionClientOptions}'; + } +} diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart index 36f69ba03e8..65882970a63 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/transport/dio/dio_requester.dart @@ -117,7 +117,7 @@ class DioRequester implements Requester { path: request.path, ); if (request.queryParameters.isNotEmpty) { - return Uri.dataFromString( + return Uri.parse( "${uri.toString()}?${request.queryParameters.entries.map((e) => "${e.key}=${e.value}").join("&")}"); } diff --git a/clients/algoliasearch-client-dart/packages/client_ingestion/lib/src/extension.dart b/clients/algoliasearch-client-dart/packages/client_ingestion/lib/src/extension.dart new file mode 100644 index 00000000000..e69de29bb2d diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart index 8858090b4cc..3733ea7b478 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart @@ -1,2 +1,3 @@ export 'extension/search.dart'; +export 'extension/transformation.dart'; export 'extension/wait_task.dart'; diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart new file mode 100644 index 00000000000..6fddc32b428 --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/transformation.dart @@ -0,0 +1,258 @@ +import 'dart:math'; + +import 'package:algolia_client_core/algolia_client_core.dart'; +import 'package:algolia_client_ingestion/algolia_client_ingestion.dart' as ingestion; +import 'package:algolia_client_search/src/api/search_client.dart'; +import 'package:algolia_client_search/src/extension/wait_task.dart'; +import 'package:algolia_client_search/src/model/event.dart'; +import 'package:algolia_client_search/src/model/event_status.dart'; +import 'package:algolia_client_search/src/model/event_type.dart'; +import 'package:algolia_client_search/src/model/operation_index_params.dart'; +import 'package:algolia_client_search/src/model/operation_type.dart'; +import 'package:algolia_client_search/src/model/replace_all_objects_with_transformation_response.dart'; +import 'package:algolia_client_search/src/model/scope_type.dart'; +import 'package:algolia_client_search/src/model/watch_response.dart'; + +extension Transformation on SearchClient { + static const _notSetError = + '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'; + + /// Chunks [objects] and pushes them through the Ingestion pipeline with [action]. + Future> chunkedPush({ + required String indexName, + required Iterable> objects, + required ingestion.Action action, + bool waitForTasks = false, + int batchSize = 1000, + String? referenceIndexName, + RequestOptions? requestOptions, + }) async { + if (batchSize < 1) throw ArgumentError('`batchSize` must be greater than 0'); + final transporter = ingestionTransporter; + if (transporter == null) throw StateError(_notSetError); + + final responses = []; + final batch = []; + final pollInterval = (batchSize ~/ 10).clamp(1, batchSize); + int polledUpTo = 0; + + final iter = objects.iterator; + if (!iter.moveNext()) return responses; + + while (true) { + batch.add(_toRecord(iter.current)); + final isLast = !iter.moveNext(); + + if (batch.length == batchSize || isLast) { + final raw = await transporter.push( + indexName: indexName, + pushTaskPayload: ingestion.PushTaskPayload(action: action, records: List.of(batch)), + referenceIndexName: referenceIndexName, + requestOptions: requestOptions, + ); + responses.add(_convertWatchResponse(raw)); + batch.clear(); + + if (waitForTasks && + (responses.length % pollInterval == 0 || isLast)) { + await _pollBatch( + transporter: transporter, + responses: responses, + from: polledUpTo, + to: responses.length, + requestOptions: requestOptions, + ); + polledUpTo = responses.length; + } + } + + if (isLast) break; + } + + return responses; + } + + /// Saves objects through the Ingestion pipeline. Requires [TransformationOptions] to be set. + Future> saveObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + bool waitForTasks = false, + int batchSize = 1000, + RequestOptions? requestOptions, + }) { + return chunkedPush( + indexName: indexName, + objects: objects, + action: ingestion.Action.addObject, + waitForTasks: waitForTasks, + batchSize: batchSize, + requestOptions: requestOptions, + ); + } + + /// Partially updates objects through the Ingestion pipeline. Requires [TransformationOptions] to be set. + Future> partialUpdateObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + bool createIfNotExists = true, + bool waitForTasks = false, + int batchSize = 1000, + RequestOptions? requestOptions, + }) { + return chunkedPush( + indexName: indexName, + objects: objects, + action: createIfNotExists + ? ingestion.Action.partialUpdateObject + : ingestion.Action.partialUpdateObjectNoCreate, + waitForTasks: waitForTasks, + batchSize: batchSize, + requestOptions: requestOptions, + ); + } + + /// Replaces all objects in [indexName] via the Ingestion pipeline without downtime. + /// Requires [TransformationOptions] to be set. + Future replaceAllObjectsWithTransformation({ + required String indexName, + required Iterable> objects, + int batchSize = 1000, + List? scopes, + RequestOptions? requestOptions, + }) async { + if (ingestionTransporter == null) throw StateError(_notSetError); + + final effectiveScopes = scopes ?? [ScopeType.settings, ScopeType.rules, ScopeType.synonyms]; + final tmpIndex = '${indexName}_tmp_${Random().nextInt(900000) + 100000}'; + + try { + var copyResponse = await operationIndex( + indexName: indexName, + operationIndexParams: OperationIndexParams( + operation: OperationType.copy, + destination: tmpIndex, + scope: effectiveScopes, + ), + requestOptions: requestOptions, + ); + + final watchResponses = await chunkedPush( + indexName: tmpIndex, + objects: objects, + action: ingestion.Action.addObject, + waitForTasks: true, + batchSize: batchSize, + referenceIndexName: indexName, + requestOptions: requestOptions, + ); + + await waitTask(indexName: tmpIndex, taskID: copyResponse.taskID, requestOptions: requestOptions); + + copyResponse = await operationIndex( + indexName: indexName, + operationIndexParams: OperationIndexParams( + operation: OperationType.copy, + destination: tmpIndex, + scope: effectiveScopes, + ), + requestOptions: requestOptions, + ); + await waitTask(indexName: tmpIndex, taskID: copyResponse.taskID, requestOptions: requestOptions); + + final moveResponse = await operationIndex( + indexName: tmpIndex, + operationIndexParams: OperationIndexParams( + operation: OperationType.move, + destination: indexName, + ), + requestOptions: requestOptions, + ); + await waitTask(indexName: tmpIndex, taskID: moveResponse.taskID, requestOptions: requestOptions); + + return ReplaceAllObjectsWithTransformationResponse( + copyOperationResponse: copyResponse, + watchResponses: watchResponses, + moveOperationResponse: moveResponse, + ); + } catch (_) { + try { + await deleteIndex(indexName: tmpIndex); + } catch (_) {} + rethrow; + } + } +} + +Future _pollBatch({ + required ingestion.IngestionClient transporter, + required List responses, + required int from, + required int to, + RequestOptions? requestOptions, +}) async { + for (final resp in responses.sublist(from, to)) { + final eventID = resp.eventID; + if (eventID == null) continue; + await _waitForEvent( + transporter: transporter, + runID: resp.runID, + eventID: eventID, + requestOptions: requestOptions, + ); + } +} + +Future _waitForEvent({ + required ingestion.IngestionClient transporter, + required String runID, + required String eventID, + RequestOptions? requestOptions, +}) async { + for (var retries = 0; retries < 50; retries++) { + try { + await transporter.getEvent( + runID: runID, + eventID: eventID, + requestOptions: requestOptions, + ); + return; + } on AlgoliaApiException catch (e) { + if (e.statusCode != 404) rethrow; + } + await Future.delayed( + Duration(milliseconds: (retries * 1500).clamp(0, 5000)), + ); + } +} + +ingestion.PushTaskRecords _toRecord(Map obj) { + final objectID = obj['objectID']; + if (objectID == null || objectID is! String) { + throw ArgumentError('each object must have an `objectID` key in order to be indexed'); + } + final rest = Map.from(obj)..remove('objectID'); + return ingestion.PushTaskRecords(objectID: objectID, additionalProperties: rest); +} + +WatchResponse _convertWatchResponse(ingestion.WatchResponse r) { + return WatchResponse( + runID: r.runID, + eventID: r.eventID, + data: r.data, + events: r.events + ?.map((e) => Event( + eventID: e.eventID, + runID: e.runID, + status: e.status != null ? EventStatus.fromJson(e.status!.toJson()) : null, + type: EventType.fromJson(e.type.toJson()), + batchSize: e.batchSize, + data: e.data, + publishedAt: e.publishedAt, + )) + .toList(), + message: r.message, + createdAt: r.createdAt, + ); +} diff --git a/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml b/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml index d45b78449b4..ad09e0a0702 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml +++ b/clients/algoliasearch-client-dart/packages/client_search/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: algolia_client_core: ^1.49.1 + algolia_client_ingestion: ^1.49.1 json_annotation: ^4.8.1 collection: ^1.17.1 diff --git a/config/clients.config.json b/config/clients.config.json index d9227dd9630..7c191fd8f12 100644 --- a/config/clients.config.json +++ b/config/clients.config.json @@ -48,6 +48,10 @@ "name": "composition", "output": "clients/algoliasearch-client-dart/packages/client_composition" }, + { + "name": "ingestion", + "output": "clients/algoliasearch-client-dart/packages/client_ingestion" + }, { "name": "insights", "output": "clients/algoliasearch-client-dart/packages/client_insights" diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java index 737ad73f16f..9bc12a4fc4a 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaDartGenerator.java @@ -114,6 +114,7 @@ public void processOpts() { public void processOpenAPI(OpenAPI openAPI) { super.processOpenAPI(openAPI); Helpers.generateServers(super.fromServers(openAPI.getServers()), additionalProperties); + Timeouts.enrichBundle(openAPI, additionalProperties); } @Override diff --git a/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java b/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java index 4a8f3e8a670..c2b8a974b6f 100644 --- a/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java +++ b/generators/src/main/java/com/algolia/codegen/cts/manager/DartCTSManager.java @@ -59,8 +59,25 @@ public void addMustacheLambdas(Map lambdas) { if (text.equals("external")) { writer.write("external_"); } else { - writer.write(text); + writer.write(snakeToCamel(text)); } }); } + + private static String snakeToCamel(String s) { + if (!s.contains("_")) return s; + StringBuilder sb = new StringBuilder(); + boolean nextUpper = false; + for (char c : s.toCharArray()) { + if (c == '_') { + nextUpper = true; + } else if (nextUpper) { + sb.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } } diff --git a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java index 2b09bd2fb1a..11cebaae1d2 100644 --- a/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java +++ b/generators/src/main/java/com/algolia/codegen/utils/Timeouts.java @@ -46,5 +46,10 @@ public static void enrichBundle(OpenAPI spec, Map bundle) throws } bundle.put("x-timeouts", specTimeouts); + + // Flat scalars for templates that cannot traverse nested Java objects via Mustache. + bundle.put("serverConnectTimeout", specTimeouts.server.connect); + bundle.put("serverReadTimeout", specTimeouts.server.read); + bundle.put("serverWriteTimeout", specTimeouts.server.write); } } diff --git a/playground/dart/pubspec_overrides.yaml b/playground/dart/pubspec_overrides.yaml index ec9972620fd..dddb30ec12e 100644 --- a/playground/dart/pubspec_overrides.yaml +++ b/playground/dart/pubspec_overrides.yaml @@ -3,6 +3,8 @@ dependency_overrides: path: ../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: diff --git a/scripts/cts/runCts.ts b/scripts/cts/runCts.ts index 14dd786b6bb..2828e4df5bf 100644 --- a/scripts/cts/runCts.ts +++ b/scripts/cts/runCts.ts @@ -184,14 +184,14 @@ export async function runCts( assertChunkWrapperValid(languages.length - skip('dart')); assertValidReplaceAllObjects(languages.length - skip('dart')); assertValidReplaceAllObjectsWithTransformation( - only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala'), + only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala') + only('dart'), ); assertValidAccountCopyIndex(only('javascript')); assertValidReplaceAllObjectsFailed(languages.length - skip('dart')); assertValidReplaceAllObjectsScopes(languages.length - skip('dart')); assertValidWaitForApiKey(languages.length - skip('dart')); assertPushMockValid( - only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala'), + only('javascript') + only('go') + only('python') + only('java') + only('php') + only('csharp') + only('scala') + only('dart'), ); } if (withBenchmarkServer) { diff --git a/specs/search/helpers/partialUpdateObjectsWithTransformation.yml b/specs/search/helpers/partialUpdateObjectsWithTransformation.yml index ae256787dfa..96eb16621c5 100644 --- a/specs/search/helpers/partialUpdateObjectsWithTransformation.yml +++ b/specs/search/helpers/partialUpdateObjectsWithTransformation.yml @@ -3,6 +3,7 @@ method: x-helper: true x-available-languages: - csharp + - dart - go - java - javascript diff --git a/specs/search/helpers/replaceAllObjectsWithTransformation.yml b/specs/search/helpers/replaceAllObjectsWithTransformation.yml index c4554b4c850..c4b3bad0cc1 100644 --- a/specs/search/helpers/replaceAllObjectsWithTransformation.yml +++ b/specs/search/helpers/replaceAllObjectsWithTransformation.yml @@ -5,6 +5,7 @@ method: - Records x-available-languages: - csharp + - dart - go - java - javascript diff --git a/specs/search/helpers/saveObjectsWithTransformation.yml b/specs/search/helpers/saveObjectsWithTransformation.yml index 090492bf3f3..a0088d4262e 100644 --- a/specs/search/helpers/saveObjectsWithTransformation.yml +++ b/specs/search/helpers/saveObjectsWithTransformation.yml @@ -5,6 +5,7 @@ method: - Records x-available-languages: - csharp + - dart - go - java - javascript diff --git a/templates/dart/api.mustache b/templates/dart/api.mustache index ca7d956bae6..7a280014af6 100644 --- a/templates/dart/api.mustache +++ b/templates/dart/api.mustache @@ -7,6 +7,9 @@ import 'package:{{pubName}}/src/version.dart'; {{#operations}} {{#imports}}import '{{{.}}}'; {{/imports}} +{{#isSearchClient}} +import 'package:algolia_client_ingestion/algolia_client_ingestion.dart' as ingestion; +{{/isSearchClient}} {{#description}} /// {{{description}}} @@ -22,6 +25,12 @@ final class {{classname}} implements ApiClient { final RetryStrategy _retryStrategy; + {{#isSearchClient}} + final String _appId; + final String _apiKey; + ingestion.IngestionClient? _ingestionTransporter; + {{/isSearchClient}} + {{classname}}({ required String appId, required String apiKey, @@ -29,7 +38,7 @@ final class {{classname}} implements ApiClient { {{#hasRegionalHost}} {{^fallbackToAliasHost}}required{{/fallbackToAliasHost}} this.region, {{/hasRegionalHost}} - }) : _retryStrategy = RetryStrategy.create( + }) : {{#isSearchClient}}_appId = appId, _apiKey = apiKey, {{/isSearchClient}}_retryStrategy = RetryStrategy.create( segment: AgentSegment(value: "{{{baseName}}}", version: packageVersion), appId: appId, apiKey: apiKey, @@ -72,6 +81,11 @@ final class {{classname}} implements ApiClient { ) { assert(appId.isNotEmpty, '`appId` is missing.'); assert(apiKey.isNotEmpty, '`apiKey` is missing.'); + {{#isSearchClient}} + if (options.transformationOptions != null) { + _ingestionTransporter = _buildIngestionTransporter(options.transformationOptions!); + } + {{/isSearchClient}} } /// Allows to switch the API key used to authenticate requests. @@ -79,6 +93,41 @@ final class {{classname}} implements ApiClient { void setClientApiKey({required String apiKey}) { this._retryStrategy.requester.setClientApiKey(apiKey); } + + {{#isSearchClient}} + /// Sets (or replaces) the ingestion transporter used by `*WithTransformation` helpers. + /// The new transporter is built from [transformationOptions]; any previously created transporter is disposed. + void setTransformationOptions(TransformationOptions transformationOptions) { + final previous = _ingestionTransporter; + _ingestionTransporter = _buildIngestionTransporter(transformationOptions); + previous?.dispose(); + } + + /// The ingestion transporter used by `*WithTransformation` helpers, or `null` if not configured. + ingestion.IngestionClient? get ingestionTransporter => _ingestionTransporter; + + ingestion.IngestionClient _buildIngestionTransporter(TransformationOptions opts) { + final ingestionOpts = opts.ingestionClientOptions; + return ingestion.IngestionClient( + appId: _appId, + apiKey: _apiKey, + region: opts.region, + options: ClientOptions( + connectTimeout: ingestionOpts?.connectTimeout ?? const Duration(seconds: 25), + readTimeout: ingestionOpts?.readTimeout ?? const Duration(seconds: 25), + writeTimeout: ingestionOpts?.writeTimeout ?? const Duration(seconds: 25), + hosts: ingestionOpts?.hosts, + headers: ingestionOpts?.headers, + requester: ingestionOpts?.requester, + logger: ingestionOpts?.logger, + interceptors: ingestionOpts?.interceptors, + httpClientAdapter: ingestionOpts?.httpClientAdapter, + compression: ingestionOpts?.compression, + agentSegments: ingestionOpts?.agentSegments, + ), + ); + } + {{/isSearchClient}} {{#operation}} /// {{{notes}}}{{#vendorExtensions}}{{#x-acl.0}} @@ -172,6 +221,10 @@ final class {{classname}} implements ApiClient { writeTimeout: Duration(milliseconds: {{write}}), readTimeout: Duration(milliseconds: {{read}}), connectTimeout: Duration(milliseconds: {{connect}}), + ) + {{/vendorExtensions.x-timeouts}}{{^vendorExtensions.x-timeouts}}RequestOptions( + writeTimeout: Duration(milliseconds: {{{serverWriteTimeout}}}), + readTimeout: Duration(milliseconds: {{{serverReadTimeout}}}), + connectTimeout: Duration(milliseconds: {{{serverConnectTimeout}}}), ) + {{/vendorExtensions.x-timeouts}}requestOptions, ); {{#returnType}} @@ -221,6 +274,9 @@ final class {{classname}} implements ApiClient { {{/isCompositionClient}} @override - void dispose() => _retryStrategy.dispose(); + void dispose() { + _retryStrategy.dispose(); + {{#isSearchClient}}_ingestionTransporter?.dispose();{{/isSearchClient}} + } } {{/operations}} diff --git a/templates/dart/pubspec.mustache b/templates/dart/pubspec.mustache index c5401541167..53f80464d34 100644 --- a/templates/dart/pubspec.mustache +++ b/templates/dart/pubspec.mustache @@ -21,6 +21,9 @@ dependencies: algolia_client_search: ^{{{searchVersion}}} algolia_client_insights: ^{{{insightsVersion}}} {{/isAlgoliasearchClient}} + {{#isSearchClient}} + algolia_client_ingestion: ^{{{coreVersion}}} + {{/isSearchClient}} json_annotation: ^4.8.1 collection: ^1.17.1 diff --git a/templates/dart/pubspec.tests.mustache b/templates/dart/pubspec.tests.mustache index 419fb3820e5..89697695be3 100644 --- a/templates/dart/pubspec.tests.mustache +++ b/templates/dart/pubspec.tests.mustache @@ -18,6 +18,7 @@ dependencies: test_api: ^0.7.3 algolia_client_core: any + algolia_client_ingestion: any dev_dependencies: - lints: ^6.0.0 \ No newline at end of file + lints: ^6.0.0 diff --git a/templates/dart/pubspec_overrides.tests.mustache b/templates/dart/pubspec_overrides.tests.mustache index 5b2a15fb576..839034f23d6 100644 --- a/templates/dart/pubspec_overrides.tests.mustache +++ b/templates/dart/pubspec_overrides.tests.mustache @@ -3,6 +3,8 @@ dependency_overrides: path: ../../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: diff --git a/templates/dart/tests/client/client.mustache b/templates/dart/tests/client/client.mustache index d7fe1bd07f5..dccf2fb4ce0 100644 --- a/templates/dart/tests/client/client.mustache +++ b/templates/dart/tests/client/client.mustache @@ -3,13 +3,12 @@ import '{{{import}}}'; import 'package:algolia_test/algolia_test.dart'; import 'package:test/test.dart'; import 'package:test_api/hooks.dart'; -import 'dart:io' show Platform; +import 'dart:io' as io; void main() { {{#blocksClient}} {{#tests}} - {{^isHelper}} {{! Helper tests are not supported yet}} - test('{{{testName}}}', () async { + test('{{{testName}}}', () async { final requester = RequestInterceptor(); {{#autoCreateClient}} final client = {{client}}( @@ -56,7 +55,6 @@ void main() { } ); - {{/isHelper}} {{/tests}} {{/blocksClient}} } \ No newline at end of file diff --git a/templates/dart/tests/client/createClient.mustache b/templates/dart/tests/client/createClient.mustache index 62b0c9d566f..1e5f0a242df 100644 --- a/templates/dart/tests/client/createClient.mustache +++ b/templates/dart/tests/client/createClient.mustache @@ -1 +1,4 @@ -{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); \ No newline at end of file +{{^autoCreateClient}}final client = {{/autoCreateClient}}{{client}}(appId : "{{parametersWithDataTypeMap.appId.value}}", apiKey : "{{parametersWithDataTypeMap.apiKey.value}}",{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}}region: '{{parametersWithDataTypeMap.region.value}}',{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}options: ClientOptions({{#gzipEncoding}}compression: 'gzip',{{/gzipEncoding}}{{#useEchoRequester}}requester: requester{{/useEchoRequester}}{{#hasCustomHosts}}hosts:[{{#customHosts}}Host.create(url: (io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/customHosts}}]{{/hasCustomHosts}})); +{{#hasTransformationOptions}} +client.setTransformationOptions(TransformationOptions(region: "{{{transformationRegion}}}"{{#hasTransformationCustomHosts}}, ingestionClientOptions: ClientOptions(hosts: [{{#transformationCustomHosts}}Host.create(url: (io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal') + ':{{port}}', scheme: 'http'),{{/transformationCustomHosts}}]){{/hasTransformationCustomHosts}})); +{{/hasTransformationOptions}} \ No newline at end of file diff --git a/templates/dart/tests/client/method.mustache b/templates/dart/tests/client/method.mustache index 23e87357c92..6cbc696f7fe 100644 --- a/templates/dart/tests/client/method.mustache +++ b/templates/dart/tests/client/method.mustache @@ -3,6 +3,7 @@ try { {{#parametersWithDataType}} {{> tests/request_param}} {{/parametersWithDataType}} + {{#hasRequestOptions}}requestOptions: RequestOptions({{#requestOptions.timeouts}}{{#write}}writeTimeout: Duration(milliseconds: {{{.}}}),{{/write}}{{#read}}readTimeout: Duration(milliseconds: {{{.}}}),{{/read}}{{#connect}}connectTimeout: Duration(milliseconds: {{{.}}}),{{/connect}}{{/requestOptions.timeouts}}),{{/hasRequestOptions}} ); {{#testResponse}} {{^match.isPrimitive}} diff --git a/templates/dart/tests/param_object.mustache b/templates/dart/tests/param_object.mustache index b01ef3bc9b2..d6699f6c0fc 100644 --- a/templates/dart/tests/param_object.mustache +++ b/templates/dart/tests/param_object.mustache @@ -2,4 +2,7 @@ {{#value}} {{> tests/request_param}} {{/value}} +{{#hasAdditionalProperties}} +additionalProperties: { {{#value}}{{#isAdditionalProperty}}'{{{key}}}': {{> tests/param_value}}, {{/isAdditionalProperty}}{{/value}} }, +{{/hasAdditionalProperties}} ) \ No newline at end of file diff --git a/templates/dart/tests/request_param.mustache b/templates/dart/tests/request_param.mustache index b27bf4cfb15..83118450721 100644 --- a/templates/dart/tests/request_param.mustache +++ b/templates/dart/tests/request_param.mustache @@ -1,6 +1,3 @@ {{^isAdditionalProperty}} {{#lambda.identifier}}{{{key}}}{{/lambda.identifier}} : {{> tests/param_value}}, -{{/isAdditionalProperty}} -{{#isAdditionalProperty}} -additionalProperties : { '{{{key}}}' : '{{{value}}}' }, {{/isAdditionalProperty}} \ No newline at end of file diff --git a/tests/CTS/client/ingestion/api.json b/tests/CTS/client/ingestion/api.json index b5c9f063cf7..1d756400fd7 100644 --- a/tests/CTS/client/ingestion/api.json +++ b/tests/CTS/client/ingestion/api.json @@ -33,7 +33,8 @@ "python": "Too Many Requests", "ruby": "429: Too Many Requests", "scala": "429 Too Many Requests", - "swift": "HTTP error: Status code: 429 Message: No message" + "swift": "HTTP error: Status code: 429 Message: No message", + "dart": "429" } } } diff --git a/tests/output/dart/lib/src/expect.dart b/tests/output/dart/lib/src/expect.dart index ca0a7016ba6..f73c0b89932 100644 --- a/tests/output/dart/lib/src/expect.dart +++ b/tests/output/dart/lib/src/expect.dart @@ -72,6 +72,9 @@ Future expectError(String message, Function block) async { } on UnreachableHostsException catch (e) { expect(e.toString(), message); return; + } on AlgoliaApiException catch (e) { + expect(e.toString(), contains(message)); + return; } assert(false); diff --git a/tests/output/dart/pubspec.yaml b/tests/output/dart/pubspec.yaml index 910e241de52..91af6e62b50 100644 --- a/tests/output/dart/pubspec.yaml +++ b/tests/output/dart/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: test_api: ^0.7.3 algolia_client_core: any + algolia_client_ingestion: any dev_dependencies: - lints: ^6.0.0 \ No newline at end of file + lints: ^6.0.0 diff --git a/tests/output/dart/pubspec_overrides.yaml b/tests/output/dart/pubspec_overrides.yaml index 5b2a15fb576..839034f23d6 100644 --- a/tests/output/dart/pubspec_overrides.yaml +++ b/tests/output/dart/pubspec_overrides.yaml @@ -3,6 +3,8 @@ dependency_overrides: path: ../../../clients/algoliasearch-client-dart/packages/algoliasearch algolia_client_core: path: ../../../clients/algoliasearch-client-dart/packages/client_core + algolia_client_ingestion: + path: ../../../clients/algoliasearch-client-dart/packages/client_ingestion algolia_client_search: path: ../../../clients/algoliasearch-client-dart/packages/client_search algolia_client_insights: diff --git a/tests/output/dart/test/client/search_test.dart b/tests/output/dart/test/client/search_test.dart index 10849f23921..dab72d612d7 100644 --- a/tests/output/dart/test/client/search_test.dart +++ b/tests/output/dart/test/client/search_test.dart @@ -3,7 +3,7 @@ import 'package:algolia_client_search/algolia_client_search.dart'; import 'package:algolia_test/algolia_test.dart'; import 'package:test/test.dart'; import 'package:test_api/hooks.dart'; -import 'dart:io' show Platform; +import 'dart:io' as io; void main() { test('calls api with correct read host', () async { @@ -68,15 +68,15 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6677', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6677', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -98,7 +98,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6676', scheme: 'http'), ])); await expectError( @@ -123,15 +123,15 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6671', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6671', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6672', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6672', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6673', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6673', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -153,7 +153,7 @@ void main() { options: ClientOptions(compression: 'gzip', hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6678', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -181,7 +181,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6691', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6691', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -242,7 +242,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -265,7 +265,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); requester.setOnRequest((request) {}); @@ -289,11 +289,11 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6675', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6675', scheme: 'http'), Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6674', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6674', scheme: 'http'), ])); { @@ -469,7 +469,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6686', scheme: 'http'), ])); try { @@ -492,7 +492,7 @@ void main() { options: ClientOptions(hosts: [ Host.create( url: - '${Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6683', + '${io.Platform.environment['CI'] == 'true' ? 'localhost' : 'host.docker.internal'}:6683', scheme: 'http'), ])); {