From a71ed4d059c4f5d63c701a5985ed1115241ab728 Mon Sep 17 00:00:00 2001 From: Anita Ruangrotsakun <138700973+anitarua@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:44:41 -0800 Subject: [PATCH] feat: cache client control plane operations (#44) * feat: cache client control plane operations * pass auth token to ci * remove unnecessary commented out tests * empty commit to nudge ci * shorten response name to AlreadyExists --- .github/workflows/dart.yml | 2 + examples/topics.dart | 2 +- lib/client_sdk_dart.dart | 4 + lib/src/cache_client.dart | 42 +++++++-- lib/src/config/cache_configurations.dart | 2 +- lib/src/config/topic_configurations.dart | 2 +- lib/src/internal/control_client.dart | 86 +++++++++++++++++++ lib/src/internal/data_client.dart | 10 +-- .../cache/control/create_cache_response.dart | 33 +++++++ .../cache/control/delete_cache_response.dart | 28 ++++++ .../cache/control/list_caches_response.dart | 46 ++++++++++ pubspec.yaml | 1 + test/src/cache/cache_test.dart | 84 ++++++++++++++++++ test/src/cache/test_utils.dart | 32 +++++++ 14 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 lib/src/internal/control_client.dart create mode 100644 lib/src/messages/responses/cache/control/create_cache_response.dart create mode 100644 lib/src/messages/responses/cache/control/delete_cache_response.dart create mode 100644 lib/src/messages/responses/cache/control/list_caches_response.dart create mode 100644 test/src/cache/cache_test.dart create mode 100644 test/src/cache/test_utils.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 2fcb412..1bcbc81 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -40,3 +40,5 @@ jobs: # want to change this to 'flutter test'. - name: Run tests run: dart test + env: + TEST_API_KEY: ${{ secrets.ALPHA_TEST_AUTH_TOKEN }} diff --git a/examples/topics.dart b/examples/topics.dart index f590b21..fb0f1bd 100644 --- a/examples/topics.dart +++ b/examples/topics.dart @@ -12,7 +12,7 @@ void main() async { var topicClient = TopicClient( CredentialProvider.fromEnvironmentVariable("MOMENTO_API_KEY"), - Mobile.latest()); + MobileTopicConfiguration.latest()); // start publishing messages in 2 seconds Timer(const Duration(seconds: 2), () async { diff --git a/lib/client_sdk_dart.dart b/lib/client_sdk_dart.dart index 7b1241f..ac91414 100644 --- a/lib/client_sdk_dart.dart +++ b/lib/client_sdk_dart.dart @@ -7,10 +7,14 @@ export 'src/topic_client.dart'; export 'src/cache_client.dart'; export 'src/auth/credential_provider.dart'; export 'src/config/topic_configurations.dart'; +export 'src/config/cache_configurations.dart'; export 'src/messages/values.dart'; export 'src/messages/responses/topics/topic_publish.dart'; export 'src/messages/responses/topics/topic_subscribe.dart'; export 'src/messages/responses/topics/topic_subscription_item.dart'; export 'src/config/logger.dart' show LogLevel; +export 'src/messages/responses/cache/control/create_cache_response.dart'; +export 'src/messages/responses/cache/control/delete_cache_response.dart'; +export 'src/messages/responses/cache/control/list_caches_response.dart'; // TODO: Export any libraries intended for clients of this package. diff --git a/lib/src/cache_client.dart b/lib/src/cache_client.dart index daac88c..f147e90 100644 --- a/lib/src/cache_client.dart +++ b/lib/src/cache_client.dart @@ -1,14 +1,19 @@ +import 'package:client_sdk_dart/client_sdk_dart.dart'; import 'package:client_sdk_dart/src/config/cache_configuration.dart'; +import 'package:client_sdk_dart/src/internal/control_client.dart'; import 'package:client_sdk_dart/src/internal/data_client.dart'; import 'package:client_sdk_dart/src/messages/responses/cache/data/scalar/get_response.dart'; import 'package:client_sdk_dart/src/messages/responses/cache/data/scalar/set_response.dart'; import 'package:logging/logging.dart'; - -import 'auth/credential_provider.dart'; -import 'config/logger.dart'; -import 'messages/values.dart'; +// import 'config/logger.dart'; abstract class ICacheClient { + Future createCache(String cacheName); + + Future deleteCache(String cacheName); + + Future listCaches(); + Future get(String cacheName, Value value); Future set(String cacheName, Value key, Value value, @@ -16,17 +21,36 @@ abstract class ICacheClient { } class CacheClient implements ICacheClient { - final DataClient _dataClient; + late final DataClient _dataClient; + late final ControlClient _controlClient; final Logger _logger = Logger('MomentoCacheClient'); CacheClient(CredentialProvider credentialProvider, - CacheConfiguration configuration, Duration defaultTtl) - : _dataClient = - DataClient(credentialProvider, configuration, defaultTtl) { - _logger.level = determineLoggerLevel(configuration.logLevel); + CacheConfiguration configuration, Duration defaultTtl) { + _dataClient = DataClient(credentialProvider, configuration, defaultTtl); + _controlClient = ControlClient(credentialProvider, configuration); + // _logger.level = determineLoggerLevel(configuration.logLevel); _logger.finest("initializing cache client"); } + @override + Future createCache(String cacheName) { + // TODO: add validators + return _controlClient.createCache(cacheName); + } + + @override + Future deleteCache(String cacheName) { + // TODO: add validators + return _controlClient.deleteCache(cacheName); + } + + @override + Future listCaches() { + // TODO: add validators + return _controlClient.listCaches(); + } + @override Future get(String cacheName, Value value) { return _dataClient.get(cacheName, value); diff --git a/lib/src/config/cache_configurations.dart b/lib/src/config/cache_configurations.dart index af2e6a6..e8e173a 100644 --- a/lib/src/config/cache_configurations.dart +++ b/lib/src/config/cache_configurations.dart @@ -8,7 +8,7 @@ import 'cache_configuration.dart'; abstract interface class CacheClientConfigurations {} /// Provides prebuilt configurations for the `CacheClient` on mobile platforms -class Mobile extends CacheClientConfigurations { +class MobileCacheConfiguration extends CacheClientConfigurations { static CacheClientConfiguration defaultConfig() { return latest(); } diff --git a/lib/src/config/topic_configurations.dart b/lib/src/config/topic_configurations.dart index 39566a7..89ae6ef 100644 --- a/lib/src/config/topic_configurations.dart +++ b/lib/src/config/topic_configurations.dart @@ -7,7 +7,7 @@ import 'transport/transport_strategy.dart'; abstract interface class TopicClientConfigurations {} /// Provides prebuilt configurations for the `TopicClient` on mobile platforms -class Mobile extends TopicClientConfigurations { +class MobileTopicConfiguration extends TopicClientConfigurations { static TopicClientConfiguration defaultConfig() { return latest(); } diff --git a/lib/src/internal/control_client.dart b/lib/src/internal/control_client.dart new file mode 100644 index 0000000..12520be --- /dev/null +++ b/lib/src/internal/control_client.dart @@ -0,0 +1,86 @@ +import 'package:client_sdk_dart/client_sdk_dart.dart'; +import 'package:client_sdk_dart/generated/controlclient.pbgrpc.dart'; +import 'package:client_sdk_dart/src/config/cache_configuration.dart'; +import 'package:client_sdk_dart/src/errors/errors.dart'; +import 'package:grpc/grpc.dart'; + +abstract class AbstractControlClient { + Future createCache(String cacheName); + + Future deleteCache(String cacheName); + + Future listCaches(); +} + +class ControlClient implements AbstractControlClient { + late ClientChannel _channel; + late ScsControlClient _client; + final CacheConfiguration _configuration; + + ControlClient(CredentialProvider credentialProvider, this._configuration) { + _channel = ClientChannel(credentialProvider.controlEndpoint); + _client = ScsControlClient(_channel, + options: CallOptions(metadata: { + 'authorization': credentialProvider.apiKey, + 'agent': 'dart:0.1.0', + }, timeout: _configuration.transportStrategy.grpcConfig.deadline)); + } + + @override + Future createCache(String cacheName) async { + var request = CreateCacheRequest_(); + request.cacheName = cacheName; + try { + await _client.createCache(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return CreateCacheSuccess(); + } catch (e) { + if (e is GrpcError && e.code == StatusCode.alreadyExists) { + return AlreadyExists(); + } else if (e is SdkException) { + return CreateCacheError(e); + } else { + return CreateCacheError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future deleteCache(String cacheName) async { + var request = DeleteCacheRequest_(); + request.cacheName = cacheName; + try { + await _client.deleteCache(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return DeleteCacheSuccess(); + } catch (e) { + if (e is SdkException) { + return DeleteCacheError(e); + } else { + return DeleteCacheError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listCaches() async { + var request = ListCachesRequest_(); + try { + final resp = await _client.listCaches(request); + return ListCachesSuccess(resp.cache); + } catch (e) { + if (e is SdkException) { + return ListCachesError(e); + } else { + return ListCachesError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } +} diff --git a/lib/src/internal/data_client.dart b/lib/src/internal/data_client.dart index 3f4338b..46acab0 100644 --- a/lib/src/internal/data_client.dart +++ b/lib/src/internal/data_client.dart @@ -19,19 +19,17 @@ abstract class AbstractDataClient { class DataClient implements AbstractDataClient { late ClientChannel _channel; late ScsClient _client; - late CacheConfiguration _configuration; - late Duration _defaultTtl; + final CacheConfiguration _configuration; + final Duration _defaultTtl; - DataClient(CredentialProvider credentialProvider, - CacheConfiguration configuration, Duration defaultTtl) { + DataClient(CredentialProvider credentialProvider, this._configuration, + this._defaultTtl) { _channel = ClientChannel(credentialProvider.cacheEndpoint); _client = ScsClient(_channel, options: CallOptions(metadata: { 'authorization': credentialProvider.apiKey, 'agent': 'dart:0.1.0', }, timeout: _configuration.transportStrategy.grpcConfig.deadline)); - _configuration = configuration; - _defaultTtl = defaultTtl; } @override diff --git a/lib/src/messages/responses/cache/control/create_cache_response.dart b/lib/src/messages/responses/cache/control/create_cache_response.dart new file mode 100644 index 0000000..dd62c40 --- /dev/null +++ b/lib/src/messages/responses/cache/control/create_cache_response.dart @@ -0,0 +1,33 @@ +import 'package:client_sdk_dart/src/messages/responses/responses_base.dart'; + +/// Sealed class for a create cache response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ``` +/// switch (response) { +/// case CreateCacheSuccess(): +/// // handle success +/// case AlreadyExists(): +/// // handle already exists +/// case CreateCacheError(): +/// // handle error +/// } +/// ``` +sealed class CreateCacheResponse {} + +/// Indicates a successful create cache request. +class CreateCacheSuccess implements CreateCacheResponse {} + +/// Indicates that the cache already exists, so there was nothing to do. +class AlreadyExists implements CreateCacheResponse {} + +/// Indicates that an error occurred during the create cache request. +/// +/// The response object includes the following fields you can use to determine how you want to handle the error: +/// - `errorCode`: a unique Momento error code indicating the type of error that occurred +/// - `message`: a human-readable description of the error +/// - `innerException`: the original error that caused the failure; can be re-thrown +class CreateCacheError extends ErrorResponseBase + implements CreateCacheResponse { + CreateCacheError(super.exception); +} diff --git a/lib/src/messages/responses/cache/control/delete_cache_response.dart b/lib/src/messages/responses/cache/control/delete_cache_response.dart new file mode 100644 index 0000000..e00dc1d --- /dev/null +++ b/lib/src/messages/responses/cache/control/delete_cache_response.dart @@ -0,0 +1,28 @@ +import 'package:client_sdk_dart/src/messages/responses/responses_base.dart'; + +/// Sealed class for a delete cache response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ``` +/// switch (response) { +/// case DeleteCacheSuccess(): +/// // handle success +/// case DeleteCacheError(): +/// // handle error +/// } +/// ``` +sealed class DeleteCacheResponse {} + +/// Indicates a successful delete cache request. +class DeleteCacheSuccess implements DeleteCacheResponse {} + +/// Indicates that an error occurred during the delete cache request. +/// +/// The response object includes the following fields you can use to determine how you want to handle the error: +/// - `errorCode`: a unique Momento error code indicating the type of error that occurred +/// - `message`: a human-readable description of the error +/// - `innerException`: the original error that caused the failure; can be re-thrown +class DeleteCacheError extends ErrorResponseBase + implements DeleteCacheResponse { + DeleteCacheError(super.exception); +} diff --git a/lib/src/messages/responses/cache/control/list_caches_response.dart b/lib/src/messages/responses/cache/control/list_caches_response.dart new file mode 100644 index 0000000..761c693 --- /dev/null +++ b/lib/src/messages/responses/cache/control/list_caches_response.dart @@ -0,0 +1,46 @@ +import 'package:client_sdk_dart/generated/controlclient.pb.dart'; +import 'package:client_sdk_dart/src/messages/responses/responses_base.dart'; + +/// Represents information about a listed cache, such as its name. +/// May include additional information in the future. +class CacheInfo { + final String name; + + CacheInfo(this.name); +} + +/// Sealed class for a list caches response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ``` +/// switch (response) { +/// case ListCachesSuccess(): +/// // handle success +/// case ListCachesError(): +/// // handle error +/// } +/// ``` +sealed class ListCachesResponse {} + +/// Indicates a successful list caches request. +class ListCachesSuccess implements ListCachesResponse { + late final List caches; + + ListCachesSuccess(List grpcCaches) { + caches = grpcCaches.map((cache) => CacheInfo(cache.cacheName)).toList(); + } + + String description() { + return "[ListCachesSuccess] length of caches list: ${caches.length}"; + } +} + +/// Indicates that an error occurred during the list caches request. +/// +/// The response object includes the following fields you can use to determine how you want to handle the error: +/// - `errorCode`: a unique Momento error code indicating the type of error that occurred +/// - `message`: a human-readable description of the error +/// - `innerException`: the original error that caused the failure; can be re-thrown +class ListCachesError extends ErrorResponseBase implements ListCachesResponse { + ListCachesError(super.exception); +} diff --git a/pubspec.yaml b/pubspec.yaml index c08588a..2b37adb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: logging: ^1.2.0 protobuf: ^3.1.0 string_validator: ^1.0.2 + uuid: ^4.3.1 # path: ^1.8.0 dev_dependencies: diff --git a/test/src/cache/cache_test.dart b/test/src/cache/cache_test.dart new file mode 100644 index 0000000..67ff164 --- /dev/null +++ b/test/src/cache/cache_test.dart @@ -0,0 +1,84 @@ +import 'dart:io'; + +import 'package:client_sdk_dart/client_sdk_dart.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + late final TestSetup testSetup; + late final String integrationTestCacheName; + late final CacheClient cacheClient; + + setUpAll(() async { + testSetup = await setUpIntegrationTests(); + integrationTestCacheName = testSetup.cacheName; + cacheClient = testSetup.cacheClient; + }); + + tearDownAll(() async { + cleanUpIntegrationTests(testSetup); + }); + + group('control plane operations', () { + test('arguments are validated', () { + // TODO: after validators are added + }); + + test('caches can be created, listed, and deleted', () async { + final newTestCacheName = generateStringWithUuid("dart-create-cache"); + final createResp = await cacheClient.createCache(newTestCacheName); + switch (createResp) { + case CreateCacheSuccess(): + expect(createResp.runtimeType, CreateCacheSuccess, + reason: "create cache should succeed"); + case AlreadyExists(): + fail('Expected Success but got AlreadyExists'); + case CreateCacheError(): + fail( + 'Expected Success but got Error: ${createResp.errorCode} ${createResp.message}'); + } + + sleep(Duration(seconds: 1)); + final listResp = await cacheClient.listCaches(); + switch (listResp) { + case ListCachesSuccess(): + final cacheNames = listResp.caches.map((cacheInfo) => cacheInfo.name); + expect(cacheNames.contains(newTestCacheName), true, + reason: "new cache should be in list of caches"); + expect(cacheNames.contains(integrationTestCacheName), true, + reason: "integration test cache should be in list of caches"); + case ListCachesError(): + fail( + 'Expected Success but got Error: ${listResp.errorCode} ${listResp.message}'); + } + + sleep(Duration(seconds: 1)); + final deleteResp = await cacheClient.deleteCache(newTestCacheName); + switch (deleteResp) { + case DeleteCacheSuccess(): + expect(deleteResp.runtimeType, DeleteCacheSuccess, + reason: "delete cache should succeed"); + case DeleteCacheError(): + fail( + 'Expected Success but got Error: ${deleteResp.errorCode} ${deleteResp.message}'); + } + + sleep(Duration(seconds: 1)); + final listResp2 = await cacheClient.listCaches(); + switch (listResp2) { + case ListCachesSuccess(): + final cacheNames = + listResp2.caches.map((cacheInfo) => cacheInfo.name); + expect(cacheNames.contains(newTestCacheName), false, + reason: "new cache should no longer be in list of caches"); + expect(cacheNames.contains(integrationTestCacheName), true, + reason: + "integration test cache should still be in list of caches"); + case ListCachesError(): + fail( + 'Expected Success but got Error: ${listResp2.errorCode} ${listResp2.message}'); + } + }); + }); +} diff --git a/test/src/cache/test_utils.dart b/test/src/cache/test_utils.dart new file mode 100644 index 0000000..ce257c2 --- /dev/null +++ b/test/src/cache/test_utils.dart @@ -0,0 +1,32 @@ +import 'package:client_sdk_dart/client_sdk_dart.dart'; +import 'package:uuid/uuid.dart'; + +final apiKeyEnvVarName = "TEST_API_KEY"; +final uuidGenerator = Uuid(); + +class TestSetup { + late final String cacheName; + late final CacheClient cacheClient; + + TestSetup(this.cacheName, this.cacheClient); +} + +Future setUpIntegrationTests() async { + final cacheClient = CacheClient( + CredentialProvider.fromEnvironmentVariable(apiKeyEnvVarName), + MobileCacheConfiguration.latest(), + Duration(seconds: 30)); + final integrationTestCacheName = + generateStringWithUuid("dart-sdk-cache-tests"); + await cacheClient.createCache(integrationTestCacheName); + + return TestSetup(integrationTestCacheName, cacheClient); +} + +void cleanUpIntegrationTests(TestSetup testSetup) { + testSetup.cacheClient.deleteCache(testSetup.cacheName); +} + +String generateStringWithUuid(String prefix) { + return "$prefix-${uuidGenerator.v4()}"; +}