From 566b3feba37e8c6072b7fe3e60bffe6d6756e14f Mon Sep 17 00:00:00 2001 From: Matt Straathof <11823378+bruuuuuuuce@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:15:35 -0800 Subject: [PATCH] feat: add data client support for list api calls (#64) --- lib/momento.dart | 11 + lib/src/cache_client.dart | 209 ++++++ lib/src/internal/data_client.dart | 327 ++++++++++ lib/src/internal/utils/validators.dart | 11 + .../data/list/list_concatenate_back.dart | 34 + .../data/list/list_concatenate_front.dart | 34 + .../responses/cache/data/list/list_fetch.dart | 41 ++ .../cache/data/list/list_length.dart | 38 ++ .../cache/data/list/list_pop_back.dart | 42 ++ .../cache/data/list/list_pop_front.dart | 42 ++ .../cache/data/list/list_push_back.dart | 34 + .../cache/data/list/list_push_front.dart | 34 + .../cache/data/list/list_remove_value.dart | 28 + .../cache/data/list/list_retain.dart | 27 + lib/src/utils/collection_ttl.dart | 44 ++ test/src/cache/list_test.dart | 593 ++++++++++++++++++ test/src/cache/test_utils.dart | 34 +- 17 files changed, 1582 insertions(+), 1 deletion(-) create mode 100644 lib/src/messages/responses/cache/data/list/list_concatenate_back.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_concatenate_front.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_fetch.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_length.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_pop_back.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_pop_front.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_push_back.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_push_front.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_remove_value.dart create mode 100644 lib/src/messages/responses/cache/data/list/list_retain.dart create mode 100644 lib/src/utils/collection_ttl.dart create mode 100644 test/src/cache/list_test.dart diff --git a/lib/momento.dart b/lib/momento.dart index 799628b..7a7f08a 100644 --- a/lib/momento.dart +++ b/lib/momento.dart @@ -5,6 +5,7 @@ library momento; export 'src/topic_client.dart'; export 'src/cache_client.dart'; +export 'src/utils/collection_ttl.dart'; export 'src/auth/credential_provider.dart'; export 'src/config/topic_configurations.dart'; export 'src/config/cache_configurations.dart'; @@ -19,5 +20,15 @@ export 'src/messages/responses/cache/control/list_caches_response.dart'; export 'src/messages/responses/cache/data/scalar/delete_response.dart'; export 'src/messages/responses/cache/data/scalar/get_response.dart'; export 'src/messages/responses/cache/data/scalar/set_response.dart'; +export 'src/messages/responses/cache/data/list/list_fetch.dart'; +export 'src/messages/responses/cache/data/list/list_concatenate_back.dart'; +export 'src/messages/responses/cache/data/list/list_concatenate_front.dart'; +export 'src/messages/responses/cache/data/list/list_length.dart'; +export 'src/messages/responses/cache/data/list/list_pop_back.dart'; +export 'src/messages/responses/cache/data/list/list_pop_front.dart'; +export 'src/messages/responses/cache/data/list/list_push_back.dart'; +export 'src/messages/responses/cache/data/list/list_push_front.dart'; +export 'src/messages/responses/cache/data/list/list_remove_value.dart'; +export 'src/messages/responses/cache/data/list/list_retain.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 8d1a778..d217a6e 100644 --- a/lib/src/cache_client.dart +++ b/lib/src/cache_client.dart @@ -8,12 +8,14 @@ import 'package:momento/src/internal/utils/validators.dart'; import 'config/logger.dart'; abstract class ICacheClient { + // Control plane RPCs Future createCache(String cacheName); Future deleteCache(String cacheName); Future listCaches(); + // Unary RPCs Future get(String cacheName, Value key); Future set(String cacheName, Value key, Value value, @@ -21,6 +23,34 @@ abstract class ICacheClient { Future delete(String cacheName, Value key); + // List Collection RPCs + Future listConcatenateBack( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateFrontToSize}); + + Future listConcatenateFront( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateBackToSize}); + + Future listFetch(String cacheName, String listName, + {int? startIndex, int? endIndex}); + Future listLength(String cacheName, String listName); + Future listPopBack(String cacheName, String listName); + Future listPopFront(String cacheName, String listName); + + Future listPushBack( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateFrontToSize}); + + Future listPushFront( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateBackToSize}); + + Future listRemoveValue( + String cacheName, String listName, Value value); + + Future listRetain(String cacheName, String listName, + {int? startIndex, int? endIndex, CollectionTtl? ttl}); Future close(); } @@ -201,6 +231,185 @@ class CacheClient implements ICacheClient { /// Close the client and free up all associated resources. /// /// NOTE: the client object will not be usable after calling this method. + @override + Future listConcatenateBack( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateFrontToSize}) { + try { + validateCacheName(cacheName); + validateListName(listName); + validateList(values); + return _dataClient.listConcatenateBack(cacheName, listName, values, + ttl: ttl, truncateFrontToSize: truncateFrontToSize); + } catch (e) { + if (e is SdkException) { + return Future.value(ListConcatenateBackError(e)); + } else { + return Future.value(ListConcatenateBackError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listConcatenateFront( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateBackToSize}) { + try { + validateCacheName(cacheName); + validateListName(listName); + validateList(values); + return _dataClient.listConcatenateFront(cacheName, listName, values, + ttl: ttl, truncateBackToSize: truncateBackToSize); + } catch (e) { + if (e is SdkException) { + return Future.value(ListConcatenateFrontError(e)); + } else { + return Future.value(ListConcatenateFrontError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listFetch(String cacheName, String listName, + {int? startIndex, int? endIndex}) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listFetch(cacheName, listName, + startIndex: startIndex, endIndex: endIndex); + } catch (e) { + if (e is SdkException) { + return Future.value(ListFetchError(e)); + } else { + return Future.value(ListFetchError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listLength(String cacheName, String listName) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listLength(cacheName, listName); + } catch (e) { + if (e is SdkException) { + return Future.value(ListLengthError(e)); + } else { + return Future.value(ListLengthError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listPopBack(String cacheName, String listName) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listPopBack(cacheName, listName); + } catch (e) { + if (e is SdkException) { + return Future.value(ListPopBackError(e)); + } else { + return Future.value(ListPopBackError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listPopFront(String cacheName, String listName) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listPopFront(cacheName, listName); + } catch (e) { + if (e is SdkException) { + return Future.value(ListPopFrontError(e)); + } else { + return Future.value(ListPopFrontError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listPushBack( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateFrontToSize}) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listPushBack(cacheName, listName, value, + ttl: ttl, truncateFrontToSize: truncateFrontToSize); + } catch (e) { + if (e is SdkException) { + return Future.value(ListPushBackError(e)); + } else { + return Future.value(ListPushBackError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listPushFront( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateBackToSize}) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listPushFront(cacheName, listName, value, + ttl: ttl, truncateBackToSize: truncateBackToSize); + } catch (e) { + if (e is SdkException) { + return Future.value(ListPushFrontError(e)); + } else { + return Future.value(ListPushFrontError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listRemoveValue( + String cacheName, String listName, Value value) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listRemoveValue(cacheName, listName, value); + } catch (e) { + if (e is SdkException) { + return Future.value(ListRemoveValueError(e)); + } else { + return Future.value(ListRemoveValueError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + + @override + Future listRetain(String cacheName, String listName, + {int? startIndex, int? endIndex, CollectionTtl? ttl}) { + try { + validateCacheName(cacheName); + validateListName(listName); + return _dataClient.listRetain(cacheName, listName, + startIndex: startIndex, endIndex: endIndex, ttl: ttl); + } catch (e) { + if (e is SdkException) { + return Future.value(ListRetainError(e)); + } else { + return Future.value(ListRetainError( + UnknownException("Unexpected error: $e", null, null))); + } + } + } + @override Future close() async { await _dataClient.close(); diff --git a/lib/src/internal/data_client.dart b/lib/src/internal/data_client.dart index 0161dd0..be24f31 100644 --- a/lib/src/internal/data_client.dart +++ b/lib/src/internal/data_client.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:momento/generated/cacheclient.pbgrpc.dart'; import 'package:momento/momento.dart'; import 'package:momento/src/config/cache_configuration.dart'; @@ -6,6 +8,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:grpc/grpc.dart'; abstract class AbstractDataClient { + // Unary RPCs Future get(String cacheName, Value key); Future set(String cacheName, Value key, Value value, @@ -13,6 +16,34 @@ abstract class AbstractDataClient { Future delete(String cacheName, Value key); + // List Collection RPCs + Future listConcatenateBack( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateFrontToSize}); + + Future listConcatenateFront( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateBackToSize}); + + Future listFetch(String cacheName, String listName, + {int? startIndex, int? endIndex}); + Future listLength(String cacheName, String listName); + Future listPopBack(String cacheName, String listName); + Future listPopFront(String cacheName, String listName); + + Future listPushBack( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateFrontToSize}); + + Future listPushFront( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateBackToSize}); + + Future listRemoveValue( + String cacheName, String listName, Value value); + + Future listRetain(String cacheName, String listName, + {int? startIndex, int? endIndex, CollectionTtl? ttl}); Future close(); } @@ -103,6 +134,302 @@ class DataClient implements AbstractDataClient { } } + @override + Future listConcatenateBack( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateFrontToSize}) async { + try { + var request = ListConcatenateBackRequest_(); + request.listName = utf8.encode(listName); + request.truncateFrontToSize = truncateFrontToSize ?? 0; + request.values.addAll(values.map((e) => e.toBinary())); + CollectionTtl actualTtl = ttl ?? CollectionTtl.fromCacheTtl(); + request.ttlMilliseconds = + Int64(actualTtl.ttlMilliseconds() ?? _defaultTtl.inMilliseconds); + request.refreshTtl = actualTtl.refreshTtl(); + var response = await _client.listConcatenateBack(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListConcatenateBackSuccess(response.listLength); + } catch (e) { + if (e is GrpcError) { + return ListConcatenateBackError(grpcStatusToSdkException(e)); + } else { + return ListConcatenateBackError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listConcatenateFront( + String cacheName, String listName, List values, + {CollectionTtl? ttl, int? truncateBackToSize}) async { + try { + var request = ListConcatenateFrontRequest_(); + request.listName = utf8.encode(listName); + request.truncateBackToSize = truncateBackToSize ?? 0; + request.values.addAll(values.map((e) => e.toBinary())); + CollectionTtl actualTtl = ttl ?? CollectionTtl.fromCacheTtl(); + request.ttlMilliseconds = + Int64(actualTtl.ttlMilliseconds() ?? _defaultTtl.inMilliseconds); + request.refreshTtl = actualTtl.refreshTtl(); + var response = await _client.listConcatenateFront(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListConcatenateFrontSuccess(response.listLength); + } catch (e) { + if (e is GrpcError) { + return ListConcatenateFrontError(grpcStatusToSdkException(e)); + } else { + return ListConcatenateFrontError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listFetch(String cacheName, String listName, + {int? startIndex, int? endIndex}) async { + try { + var request = ListFetchRequest_(); + request.listName = utf8.encode(listName); + + if (startIndex != null) { + request.inclusiveStart = startIndex; + } else { + request.unboundedStart = Unbounded_(); + } + if (endIndex != null) { + request.exclusiveEnd = endIndex; + } else { + request.unboundedEnd = Unbounded_(); + } + var response = await _client.listFetch(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + switch (response.whichList()) { + case ListFetchResponse__List.found: + return ListFetchHit(response.found.values); + case ListFetchResponse__List.missing: + return ListFetchMiss(); + default: + return ListFetchError( + UnknownException("Unexpected error: $response", null, null)); + } + } catch (e) { + if (e is GrpcError) { + return ListFetchError(grpcStatusToSdkException(e)); + } else { + return ListFetchError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listLength( + String cacheName, String listName) async { + try { + var request = ListLengthRequest_(); + request.listName = utf8.encode(listName); + var response = await _client.listLength(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + switch (response.whichList()) { + case ListLengthResponse__List.found: + return ListLengthHit(response.found.length); + case ListLengthResponse__List.missing: + return ListLengthMiss(); + default: + return ListLengthError( + UnknownException("Unexpected error: $response", null, null)); + } + } catch (e) { + if (e is GrpcError) { + return ListLengthError(grpcStatusToSdkException(e)); + } else { + return ListLengthError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listPopBack( + String cacheName, String listName) async { + try { + var request = ListPopBackRequest_(); + request.listName = utf8.encode(listName); + var response = await _client.listPopBack(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + switch (response.whichList()) { + case ListPopBackResponse__List.found: + return ListPopBackHit(response.found.back); + case ListPopBackResponse__List.missing: + return ListPopBackMiss(); + default: + return ListPopBackError( + UnknownException("Unexpected error: $response", null, null)); + } + } catch (e) { + if (e is GrpcError) { + return ListPopBackError(grpcStatusToSdkException(e)); + } else { + return ListPopBackError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listPopFront( + String cacheName, String listName) async { + try { + var request = ListPopFrontRequest_(); + request.listName = utf8.encode(listName); + var response = await _client.listPopFront(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + switch (response.whichList()) { + case ListPopFrontResponse__List.found: + return ListPopFrontHit(response.found.front); + case ListPopFrontResponse__List.missing: + return ListPopFrontMiss(); + default: + return ListPopFrontError( + UnknownException("Unexpected error: $response", null, null)); + } + } catch (e) { + if (e is GrpcError) { + return ListPopFrontError(grpcStatusToSdkException(e)); + } else { + return ListPopFrontError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listPushBack( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateFrontToSize}) async { + try { + var request = ListPushBackRequest_(); + request.listName = utf8.encode(listName); + request.truncateFrontToSize = truncateFrontToSize ?? 0; + request.value = value.toBinary(); + CollectionTtl actualTtl = ttl ?? CollectionTtl.fromCacheTtl(); + request.ttlMilliseconds = + Int64(actualTtl.ttlMilliseconds() ?? _defaultTtl.inMilliseconds); + request.refreshTtl = actualTtl.refreshTtl(); + var response = await _client.listPushBack(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListPushBackSuccess(response.listLength); + } catch (e) { + if (e is GrpcError) { + return ListPushBackError(grpcStatusToSdkException(e)); + } else { + return ListPushBackError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listPushFront( + String cacheName, String listName, Value value, + {CollectionTtl? ttl, int? truncateBackToSize}) async { + try { + var request = ListPushFrontRequest_(); + request.listName = utf8.encode(listName); + request.truncateBackToSize = truncateBackToSize ?? 0; + request.value = value.toBinary(); + CollectionTtl actualTtl = ttl ?? CollectionTtl.fromCacheTtl(); + request.ttlMilliseconds = + Int64(actualTtl.ttlMilliseconds() ?? _defaultTtl.inMilliseconds); + request.refreshTtl = actualTtl.refreshTtl(); + var response = await _client.listPushFront(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListPushFrontSuccess(response.listLength); + } catch (e) { + if (e is GrpcError) { + return ListPushFrontError(grpcStatusToSdkException(e)); + } else { + return ListPushFrontError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listRemoveValue( + String cacheName, String listName, Value value) async { + try { + var request = ListRemoveRequest_(); + request.listName = utf8.encode(listName); + request.allElementsWithValue = value.toBinary(); + await _client.listRemove(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListRemoveValueSuccess(); + } catch (e) { + if (e is GrpcError) { + return ListRemoveValueError(grpcStatusToSdkException(e)); + } else { + return ListRemoveValueError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + + @override + Future listRetain(String cacheName, String listName, + {int? startIndex, int? endIndex, CollectionTtl? ttl}) async { + try { + var request = ListRetainRequest_(); + request.listName = utf8.encode(listName); + if (startIndex != null) { + request.inclusiveStart = startIndex; + } else { + request.unboundedStart = Unbounded_(); + } + if (endIndex != null) { + request.exclusiveEnd = endIndex; + } else { + request.unboundedEnd = Unbounded_(); + } + CollectionTtl actualTtl = ttl ?? CollectionTtl.fromCacheTtl(); + request.ttlMilliseconds = + Int64(actualTtl.ttlMilliseconds() ?? _defaultTtl.inMilliseconds); + request.refreshTtl = actualTtl.refreshTtl(); + await _client.listRetain(request, + options: CallOptions(metadata: { + 'cache': cacheName, + })); + return ListRetainSuccess(); + } catch (e) { + if (e is GrpcError) { + return ListRetainError(grpcStatusToSdkException(e)); + } else { + return ListRetainError( + UnknownException("Unexpected error: $e", null, null)); + } + } + } + @override Future close() async { await _channel.shutdown(); diff --git a/lib/src/internal/utils/validators.dart b/lib/src/internal/utils/validators.dart index ddba59a..c0ec621 100644 --- a/lib/src/internal/utils/validators.dart +++ b/lib/src/internal/utils/validators.dart @@ -1,5 +1,7 @@ import 'package:momento/src/errors/errors.dart'; +import '../../../momento.dart'; + void _validateString(String str, String errorMessage) { if (str.trim().isEmpty) { throw InvalidArgumentException(errorMessage, null, null); @@ -11,3 +13,12 @@ void validateCacheName(String cacheName) => void validateTopicName(String topicName) => _validateString(topicName, "Invalid topic name"); + +void validateListName(String listName) => + _validateString(listName, "Invalid list name"); + +void validateList(List values) { + if (values.isEmpty) { + throw InvalidArgumentException("List cannot be empty", null, null); + } +} diff --git a/lib/src/messages/responses/cache/data/list/list_concatenate_back.dart b/lib/src/messages/responses/cache/data/list/list_concatenate_back.dart new file mode 100644 index 0000000..f0adcdf --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_concatenate_back.dart @@ -0,0 +1,34 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list concatenate back response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListConcatenateBackSuccess(): +/// // handle successful concatenation +/// case ListConcatenateBackError(): +/// // handle error +/// } +/// ``` +sealed class ListConcatenateBackResponse {} + +/// Indicates that the request was successful and the updated length can be accessed by the field `length`. +class ListConcatenateBackSuccess implements ListConcatenateBackResponse { + ListConcatenateBackSuccess(this._length); + + final int _length; + + int get length => _length; +} + +/// Indicates that an error occurred during the list concatenate back 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 ListConcatenateBackError extends ErrorResponseBase + implements ListConcatenateBackResponse { + ListConcatenateBackError(super.exception); +} diff --git a/lib/src/messages/responses/cache/data/list/list_concatenate_front.dart b/lib/src/messages/responses/cache/data/list/list_concatenate_front.dart new file mode 100644 index 0000000..a65683d --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_concatenate_front.dart @@ -0,0 +1,34 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list concatenate front response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListConcatenateFrontSuccess(): +/// // handle successful concatenation +/// case ListConcatenateBackError(): +/// // handle error +/// } +/// ``` +sealed class ListConcatenateFrontResponse {} + +/// Indicates that the request was successful and the updated length can be accessed by the field `length`. +class ListConcatenateFrontSuccess implements ListConcatenateFrontResponse { + ListConcatenateFrontSuccess(this._length); + + final int _length; + + int get length => _length; +} + +/// Indicates that an error occurred during the list concatenate front 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 ListConcatenateFrontError extends ErrorResponseBase + implements ListConcatenateFrontResponse { + ListConcatenateFrontError(super.exception); +} diff --git a/lib/src/messages/responses/cache/data/list/list_fetch.dart b/lib/src/messages/responses/cache/data/list/list_fetch.dart new file mode 100644 index 0000000..0a874f0 --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_fetch.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:momento/src/messages/responses/responses_base.dart'; + +/// Sealed class for a cache list fetch response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListFetchHit(): +/// // handle cache hit +/// case ListFetchMiss(): +/// // handle cache miss +/// case ListFetchError(): +/// // handle error +/// } +/// ``` +sealed class ListFetchResponse {} + +/// Indicates that the requested list was not available in the cache. +class ListFetchMiss implements ListFetchResponse {} + +/// Indicates that an error occurred during the list fetch 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 ListFetchError extends ErrorResponseBase implements ListFetchResponse { + ListFetchError(super.exception); +} + +/// Indicates that the requested list was successfully retrieved from the cache and can be accessed by the fields `values` or `binaryValues`. +class ListFetchHit implements ListFetchResponse { + ListFetchHit(this._values); + + final List> _values; + + Iterable get values => _values.map((e) => utf8.decode(e)); + Iterable> get binaryValues => _values; +} diff --git a/lib/src/messages/responses/cache/data/list/list_length.dart b/lib/src/messages/responses/cache/data/list/list_length.dart new file mode 100644 index 0000000..c5cba1d --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_length.dart @@ -0,0 +1,38 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list length response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListLengthHit(): +/// // handle cache hit +/// case ListLengthMiss(): +/// // handle cache miss +/// case ListLengthError(): +/// // handle error +/// } +/// ``` +sealed class ListLengthResponse {} + +/// Indicates that the requested list was not available in the cache. +class ListLengthMiss implements ListLengthResponse {} + +/// Indicates that an error occurred during the list length 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 ListLengthError extends ErrorResponseBase implements ListLengthResponse { + ListLengthError(super.exception); +} + +/// Indicates that the requested list length was successfully retrieved from the cache and the length can be accessed by the field `length`. +class ListLengthHit implements ListLengthResponse { + ListLengthHit(this._length); + + final int _length; + + int get length => _length; +} diff --git a/lib/src/messages/responses/cache/data/list/list_pop_back.dart b/lib/src/messages/responses/cache/data/list/list_pop_back.dart new file mode 100644 index 0000000..b083661 --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_pop_back.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import '../../../responses_base.dart'; + +/// Sealed class for a cache list pop back response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListPopBackHit(): +/// // handle cache hit +/// case ListPopBackMiss(): +/// // handle cache miss +/// case ListPopBackError(): +/// // handle error +/// } +/// ``` +sealed class ListPopBackResponse {} + +/// Indicates that the requested list was not available in the cache. +class ListPopBackMiss implements ListPopBackResponse {} + +/// Indicates that an error occurred during the list pop back 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 ListPopBackError extends ErrorResponseBase + implements ListPopBackResponse { + ListPopBackError(super.exception); +} + +/// Indicates that the request was successful and the value can be accessed by the fields `value` or `binaryValue`. +class ListPopBackHit implements ListPopBackResponse { + ListPopBackHit(this._value); + + final List _value; + + String get value => utf8.decode(_value); + List get binaryValue => _value; +} diff --git a/lib/src/messages/responses/cache/data/list/list_pop_front.dart b/lib/src/messages/responses/cache/data/list/list_pop_front.dart new file mode 100644 index 0000000..6160d2e --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_pop_front.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import '../../../responses_base.dart'; + +/// Sealed class for a cache list pop front response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListPopBackHit(): +/// // handle cache hit +/// case ListPopBackMiss(): +/// // handle cache miss +/// case ListPopBackError(): +/// // handle error +/// } +/// ``` +sealed class ListPopFrontResponse {} + +/// Indicates that the requested list was not available in the cache. +class ListPopFrontMiss implements ListPopFrontResponse {} + +/// Indicates that an error occurred during the list pop front 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 ListPopFrontError extends ErrorResponseBase + implements ListPopFrontResponse { + ListPopFrontError(super.exception); +} + +/// Indicates that the request was successful and the value can be accessed by the fields `value` or `binaryValue`. +class ListPopFrontHit implements ListPopFrontResponse { + ListPopFrontHit(this._value); + + final List _value; + + String get value => utf8.decode(_value); + List get binaryValue => _value; +} diff --git a/lib/src/messages/responses/cache/data/list/list_push_back.dart b/lib/src/messages/responses/cache/data/list/list_push_back.dart new file mode 100644 index 0000000..546b498 --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_push_back.dart @@ -0,0 +1,34 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list push back response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListPushBackSuccess(): +/// // handle successful push back +/// case ListPushBackError(): +/// // handle error +/// } +/// ``` +sealed class ListPushBackResponse {} + +/// Indicates that an error occurred during the list push back 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 ListPushBackError extends ErrorResponseBase + implements ListPushBackResponse { + ListPushBackError(super.exception); +} + +/// Indicates that the request was successful and the updated length can be accessed by the field `length`. +class ListPushBackSuccess implements ListPushBackResponse { + ListPushBackSuccess(this._length); + + final int _length; + + int get length => _length; +} diff --git a/lib/src/messages/responses/cache/data/list/list_push_front.dart b/lib/src/messages/responses/cache/data/list/list_push_front.dart new file mode 100644 index 0000000..d104478 --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_push_front.dart @@ -0,0 +1,34 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list push front response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListPushFrontSuccess(): +/// // handle successful push front +/// case ListPushFrontError(): +/// // handle error +/// } +/// ``` +sealed class ListPushFrontResponse {} + +/// Indicates that an error occurred during the list push front 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 ListPushFrontError extends ErrorResponseBase + implements ListPushFrontResponse { + ListPushFrontError(super.exception); +} + +/// Indicates that the request was successful and the updated length can be accessed by the field `length`. +class ListPushFrontSuccess implements ListPushFrontResponse { + ListPushFrontSuccess(this._length); + + final int _length; + + int get length => _length; +} diff --git a/lib/src/messages/responses/cache/data/list/list_remove_value.dart b/lib/src/messages/responses/cache/data/list/list_remove_value.dart new file mode 100644 index 0000000..e0317de --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_remove_value.dart @@ -0,0 +1,28 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list remove value response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListRemoveValueSuccess(): +/// // handle successful response +/// case ListRemoveValueError(): +/// // handle error +/// } +/// ``` +sealed class ListRemoveValueResponse {} + +/// Indicates that the request was successful. +class ListRemoveValueSuccess implements ListRemoveValueResponse {} + +/// Indicates that an error occurred during the list remove value 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 ListRemoveValueError extends ErrorResponseBase + implements ListRemoveValueResponse { + ListRemoveValueError(super.exception); +} diff --git a/lib/src/messages/responses/cache/data/list/list_retain.dart b/lib/src/messages/responses/cache/data/list/list_retain.dart new file mode 100644 index 0000000..7440d15 --- /dev/null +++ b/lib/src/messages/responses/cache/data/list/list_retain.dart @@ -0,0 +1,27 @@ +import '../../../responses_base.dart'; + +/// Sealed class for a cache list retain response. +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// ```dart +/// switch (response) { +/// case ListRetainSuccess(): +/// // handle successful response +/// case ListRetainError(): +/// // handle error +/// } +/// ``` +sealed class ListRetainResponse {} + +/// Indicates that the request was successful. +class ListRetainSuccess implements ListRetainResponse {} + +/// Indicates that an error occurred during the list retain 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 ListRetainError extends ErrorResponseBase implements ListRetainResponse { + ListRetainError(super.exception); +} diff --git a/lib/src/utils/collection_ttl.dart b/lib/src/utils/collection_ttl.dart new file mode 100644 index 0000000..f67e17e --- /dev/null +++ b/lib/src/utils/collection_ttl.dart @@ -0,0 +1,44 @@ +class CollectionTtl { + late Duration? _ttlSeconds; + late bool _refreshTtl; + + CollectionTtl(Duration? ttlSeconds, bool refreshTtl) { + _ttlSeconds = ttlSeconds; + _refreshTtl = refreshTtl; + } + + int? ttlSeconds() { + return _ttlSeconds?.inSeconds; + } + + int? ttlMilliseconds() { + if (_ttlSeconds == null) { + return null; + } + return _ttlSeconds?.inMilliseconds; + } + + bool refreshTtl() { + return _refreshTtl; + } + + static CollectionTtl fromCacheTtl() { + return CollectionTtl(null, false); + } + + static CollectionTtl of(Duration ttlSeconds) { + return CollectionTtl(ttlSeconds, true); + } + + static CollectionTtl refreshTtlIfProvided(Duration? ttlSeconds) { + return CollectionTtl(ttlSeconds, ttlSeconds == null ? true : false); + } + + CollectionTtl withRefreshTtlOnUpdates() { + return CollectionTtl(_ttlSeconds, true); + } + + CollectionTtl withNoRefreshTtlOnUpdates() { + return CollectionTtl(_ttlSeconds, false); + } +} diff --git a/test/src/cache/list_test.dart b/test/src/cache/list_test.dart new file mode 100644 index 0000000..1e84c27 --- /dev/null +++ b/test/src/cache/list_test.dart @@ -0,0 +1,593 @@ +import 'package:momento/momento.dart'; +import 'package:momento/src/errors/errors.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 { + await cleanUpIntegrationTests(testSetup); + }); + + group( + 'list collection type', + () => { + group( + 'arguments are validated', + () => { + test('listFetch', () async { + final validString = "valid"; + final invalidString = " "; + + final fetchResp1 = await cacheClient.listFetch( + invalidString, validString); + switch (fetchResp1) { + case ListFetchHit(): + fail('Expected Error but got Success'); + case ListFetchMiss(): + fail('Expected Error but got Miss'); + case ListFetchError(): + expect(fetchResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listFetch should not accept empty cache name"); + } + + final fetchResp2 = await cacheClient.listFetch( + validString, invalidString); + switch (fetchResp2) { + case ListFetchHit(): + fail('Expected Error but got Success'); + case ListFetchMiss(): + fail('Expected Error but got Miss'); + case ListFetchError(): + expect(fetchResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listFetch should not accept empty list name"); + } + }), + test('listConcatenateBack', () async { + final validString = "valid"; + final invalidString = " "; + + final concatResp1 = await cacheClient + .listConcatenateBack( + invalidString, validString, []); + switch (concatResp1) { + case ListConcatenateBackSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateBackError(): + expect(concatResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateBack should not accept empty cache name"); + } + + final concatResp2 = await cacheClient + .listConcatenateBack( + validString, invalidString, []); + switch (concatResp2) { + case ListConcatenateBackSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateBackError(): + expect(concatResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateBack should not accept empty list name"); + } + + final concatResp3 = await cacheClient + .listConcatenateBack(validString, validString, []); + switch (concatResp3) { + case ListConcatenateBackSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateBackError(): + expect(concatResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateBack should not accept empty list of values"); + } + }), + test('listConcatenateFront', () async { + final validString = "valid"; + final invalidString = " "; + + final concatResp1 = await cacheClient + .listConcatenateFront( + invalidString, validString, []); + switch (concatResp1) { + case ListConcatenateFrontSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateFrontError(): + expect(concatResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateFront should not accept empty cache name"); + } + + final concatResp2 = await cacheClient + .listConcatenateFront( + validString, invalidString, []); + switch (concatResp2) { + case ListConcatenateFrontSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateFrontError(): + expect(concatResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateFront should not accept empty list name"); + } + + final concatResp3 = await cacheClient + .listConcatenateFront(validString, validString, []); + switch (concatResp3) { + case ListConcatenateFrontSuccess(): + fail('Expected Error but got Success'); + case ListConcatenateFrontError(): + expect(concatResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listConcatenateFront should not accept empty list of values"); + } + }), + test('listLength', () async { + final validString = "valid"; + final invalidString = " "; + + final lengthResp1 = await cacheClient.listLength( + invalidString, validString); + switch (lengthResp1) { + case ListLengthHit(): + fail('Expected Error but got Hit'); + case ListLengthMiss(): + fail('Expected Error but got Miss'); + case ListLengthError(): + expect(lengthResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listLength should not accept empty cache name"); + } + + final lengthResp2 = await cacheClient.listLength( + validString, invalidString); + switch (lengthResp2) { + case ListLengthHit(): + fail('Expected Error but got Hit'); + case ListLengthMiss(): + fail('Expected Error but got Miss'); + case ListLengthError(): + expect(lengthResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listLength should not accept empty list name"); + } + }), + test('listPopBack', () async { + final validString = "valid"; + final invalidString = " "; + + final popResp1 = await cacheClient.listPopBack( + invalidString, validString); + switch (popResp1) { + case ListPopBackHit(): + fail('Expected Error but got Hit'); + case ListPopBackMiss(): + fail('Expected Error but got Miss'); + case ListPopBackError(): + expect(popResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPopBack should not accept empty cache name"); + } + + final popResp2 = await cacheClient.listPopBack( + validString, invalidString); + switch (popResp2) { + case ListPopBackHit(): + fail('Expected Error but got Hit'); + case ListPopBackMiss(): + fail('Expected Error but got Miss'); + case ListPopBackError(): + expect(popResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPopBack should not accept empty list name"); + } + }), + test('listPopFront', () async { + final validString = "valid"; + final invalidString = " "; + + final popResp1 = await cacheClient.listPopFront( + invalidString, validString); + switch (popResp1) { + case ListPopFrontHit(): + fail('Expected Error but got Hit'); + case ListPopFrontMiss(): + fail('Expected Error but got Miss'); + case ListPopFrontError(): + expect(popResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPopFront should not accept empty cache name"); + } + + final popResp2 = await cacheClient.listPopFront( + validString, invalidString); + switch (popResp2) { + case ListPopFrontHit(): + fail('Expected Error but got Hit'); + case ListPopFrontMiss(): + fail('Expected Error but got Miss'); + case ListPopFrontError(): + expect(popResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPopFront should not accept empty list name"); + } + }), + test('listPushBack', () async { + final validString = "valid"; + final invalidString = " "; + + final pushResp1 = await cacheClient.listPushBack( + invalidString, validString, StringValue("string")); + switch (pushResp1) { + case ListPushBackSuccess(): + fail('Expected Error but got Success'); + case ListPushBackError(): + expect(pushResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPushBack should not accept empty cache name"); + } + + final pushResp2 = await cacheClient.listPushBack( + validString, invalidString, StringValue("string")); + switch (pushResp2) { + case ListPushBackSuccess(): + fail('Expected Error but got Success'); + case ListPushBackError(): + expect(pushResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPushBack should not accept empty list name"); + } + }), + test('listPushFront', () async { + final validString = "valid"; + final invalidString = " "; + + final pushResp1 = await cacheClient.listPushFront( + invalidString, validString, StringValue("string")); + switch (pushResp1) { + case ListPushFrontSuccess(): + fail('Expected Error but got Success'); + case ListPushFrontError(): + expect(pushResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPushFront should not accept empty cache name"); + } + + final pushResp2 = await cacheClient.listPushFront( + validString, invalidString, StringValue("string")); + switch (pushResp2) { + case ListPushFrontSuccess(): + fail('Expected Error but got Success'); + case ListPushFrontError(): + expect(pushResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listPushFront should not accept empty list name"); + } + }), + test('listRemoveValue', () async { + final validString = "valid"; + final invalidString = " "; + + final removeResp1 = await cacheClient.listRemoveValue( + invalidString, validString, StringValue("string")); + switch (removeResp1) { + case ListRemoveValueSuccess(): + fail('Expected Error but got Success'); + case ListRemoveValueError(): + expect(removeResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listRemoveValue should not accept empty cache name"); + } + + final removeResp2 = await cacheClient.listRemoveValue( + validString, invalidString, StringValue("string")); + switch (removeResp2) { + case ListRemoveValueSuccess(): + fail('Expected Error but got Success'); + case ListRemoveValueError(): + expect(removeResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listRemoveValue should not accept empty list name"); + } + }), + test('listRetain', () async { + final validString = "valid"; + final invalidString = " "; + + final retainResp1 = await cacheClient.listRetain( + invalidString, validString); + switch (retainResp1) { + case ListRetainSuccess(): + fail('Expected Error but got Success'); + case ListRetainError(): + expect(retainResp1.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listRetain should not accept empty cache name"); + } + + final retainResp2 = await cacheClient.listRetain( + validString, invalidString); + switch (retainResp2) { + case ListRetainSuccess(): + fail('Expected Error but got Success'); + case ListRetainError(): + expect(retainResp2.errorCode, + MomentoErrorCode.invalidArgumentError, + reason: + "listRetain should not accept empty list name"); + } + }), + }), + group('listFetch', () { + test('it should miss when a list doesnt exist', () async { + final nonExistentListName = + generateStringWithUuid("non-existent-list"); + final fetchResp1 = await cacheClient.listFetch( + integrationTestCacheName, nonExistentListName); + switch (fetchResp1) { + case ListFetchHit(): + fail('Expected Miss but got Success'); + case ListFetchMiss(): + // this is expected + return; + case ListFetchError(): + fail( + 'Expected Miss but got Error: ${fetchResp1.errorCode} ${fetchResp1.message}'); + } + }); + test('it should add items to a list, and return the when fetched', + () async { + final listName = generateStringWithUuid("list-name"); + final listValue = StringValue("string value"); + await cacheClient.listPushFront( + integrationTestCacheName, listName, listValue); + final fetchResp1 = await cacheClient.listFetch( + integrationTestCacheName, listName); + switch (fetchResp1) { + case ListFetchHit(): + expect(fetchResp1.values, [listValue.toUtf8()], + reason: + "list should contain the value that was pushed"); + case ListFetchMiss(): + fail('Expected Hit but got Miss'); + case ListFetchError(): + fail( + 'Expected Hit but got Error: ${fetchResp1.errorCode} ${fetchResp1.message}'); + } + }); + test('it should fetch listed items using start and end indices', + () async { + final listName = generateStringWithUuid("list-name"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront(integrationTestCacheName, + listName, [listValue1, listValue2]); + final fetchResp1 = await cacheClient.listFetch( + integrationTestCacheName, listName); + switch (fetchResp1) { + case ListFetchHit(): + expect(fetchResp1.values, + [listValue1.toUtf8(), listValue2.toUtf8()], + reason: + "list should contain the value that was pushed"); + case ListFetchMiss(): + fail('Expected Hit but got Miss'); + case ListFetchError(): + fail( + 'Expected Hit but got Error: ${fetchResp1.errorCode} ${fetchResp1.message}'); + } + + final fetchResp2 = await cacheClient.listFetch( + integrationTestCacheName, listName, + startIndex: 1); + switch (fetchResp2) { + case ListFetchHit(): + expect(fetchResp2.values, [listValue2.toUtf8()], + reason: + "list should contain the value that was pushed starting at index 1"); + case ListFetchMiss(): + fail('Expected Hit but got Miss'); + case ListFetchError(): + fail( + 'Expected Hit but got Error: ${fetchResp2.errorCode} ${fetchResp2.message}'); + } + + final fetchResp3 = await cacheClient + .listFetch(integrationTestCacheName, listName, endIndex: 1); + switch (fetchResp3) { + case ListFetchHit(): + expect(fetchResp3.values, [listValue1.toUtf8()], + reason: + "list should contain the value that was pushed up until index 1"); + case ListFetchMiss(): + fail('Expected Hit but got Miss'); + case ListFetchError(): + fail( + 'Expected Hit but got Error: ${fetchResp3.errorCode} ${fetchResp3.message}'); + } + }); + }), + group( + 'listLength', + () => { + test('it should return the length of a list', () async { + final listName = generateStringWithUuid("list-length"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront( + integrationTestCacheName, + listName, + [listValue1, listValue2]); + await verifyListLength( + integrationTestCacheName, listName, 2, cacheClient); + }), + }), + group('listPopBack', () { + test('it should pop the last item from a list', () async { + final listName = generateStringWithUuid("list-pop-back"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront(integrationTestCacheName, + listName, [listValue1, listValue2]); + final popResp1 = await cacheClient.listPopBack( + integrationTestCacheName, listName); + switch (popResp1) { + case ListPopBackHit(): + expect(popResp1.value, listValue2.toUtf8(), + reason: + "popped value should be the last value in the list"); + case ListPopBackMiss(): + fail('Expected Hit but got Miss'); + case ListPopBackError(): + fail( + 'Expected Hit but got Error: ${popResp1.errorCode} ${popResp1.message}'); + } + await verifyListLength( + integrationTestCacheName, listName, 1, cacheClient); + }); + }), + group('listPopFront', () { + test('it should pop the first item from a list', () async { + final listName = generateStringWithUuid("list-pop-front"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront(integrationTestCacheName, + listName, [listValue1, listValue2]); + final popResp1 = await cacheClient.listPopFront( + integrationTestCacheName, listName); + switch (popResp1) { + case ListPopFrontHit(): + expect(popResp1.value, listValue1.toUtf8(), + reason: + "popped value should be the first one in the list"); + case ListPopFrontMiss(): + fail('Expected Hit but got Miss'); + case ListPopFrontError(): + fail( + 'Expected Hit but got Error: ${popResp1.errorCode} ${popResp1.message}'); + } + await verifyListLength( + integrationTestCacheName, listName, 1, cacheClient); + }); + }), + group('listPushFront', () { + test('it should push items to the front of a list', () async { + final listName = generateStringWithUuid("list-push-front"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront( + integrationTestCacheName, listName, [listValue2]); + final listPushFrontResp = await cacheClient.listPushFront( + integrationTestCacheName, listName, listValue1); + switch (listPushFrontResp) { + case ListPushFrontSuccess(): + expect(listPushFrontResp.length, 2, + reason: "list should have length 2 after push"); + break; + case ListPushFrontError(): + fail( + 'Expected Success but got Error: ${listPushFrontResp.errorCode} ${listPushFrontResp.message}'); + } + await verifyListFetch(integrationTestCacheName, listName, + [listValue1, listValue2], cacheClient); + }); + }), + group('listPushBack', () { + test('it should push items to the back of a list', () async { + final listName = generateStringWithUuid("list-push-back"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront( + integrationTestCacheName, listName, [listValue1]); + final listPushBackResp = await cacheClient.listPushBack( + integrationTestCacheName, listName, listValue2); + switch (listPushBackResp) { + case ListPushBackSuccess(): + expect(listPushBackResp.length, 2, + reason: "list should have length 2 after push"); + break; + case ListPushBackError(): + fail( + 'Expected Success but got Error: ${listPushBackResp.errorCode} ${listPushBackResp.message}'); + } + await verifyListFetch(integrationTestCacheName, listName, + [listValue1, listValue2], cacheClient); + }); + }), + group('listRemoveValue', () { + test('it should remove items from a list', () async { + final listName = generateStringWithUuid("list-remove-value"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront(integrationTestCacheName, + listName, [listValue1, listValue2]); + final removeResp1 = await cacheClient.listRemoveValue( + integrationTestCacheName, listName, listValue1); + switch (removeResp1) { + case ListRemoveValueSuccess(): + // this is expected + break; + case ListRemoveValueError(): + fail( + 'Expected Success but got Error: ${removeResp1.errorCode} ${removeResp1.message}'); + } + await verifyListFetch(integrationTestCacheName, listName, + [listValue2], cacheClient); + }); + }), + group('listRetain', () { + test('it should remove items from a list', () async { + final listName = generateStringWithUuid("list-retain"); + final listValue1 = StringValue("string value 1"); + final listValue2 = StringValue("string value 2"); + await cacheClient.listConcatenateFront(integrationTestCacheName, + listName, [listValue1, listValue2]); + final retainResp1 = await cacheClient.listRetain( + integrationTestCacheName, listName, + startIndex: 1); + switch (retainResp1) { + case ListRetainSuccess(): + // this is expected + break; + case ListRetainError(): + fail( + 'Expected Success but got Error: ${retainResp1.errorCode} ${retainResp1.message}'); + } + await verifyListFetch(integrationTestCacheName, listName, + [listValue2], cacheClient); + }); + }), + }); +} diff --git a/test/src/cache/test_utils.dart b/test/src/cache/test_utils.dart index 730f998..c6252d3 100644 --- a/test/src/cache/test_utils.dart +++ b/test/src/cache/test_utils.dart @@ -1,5 +1,6 @@ import 'package:momento/momento.dart'; import 'package:uuid/uuid.dart'; +import 'package:test/test.dart'; final apiKeyEnvVarName = "TEST_API_KEY"; final uuidGenerator = Uuid(); @@ -28,5 +29,36 @@ Future cleanUpIntegrationTests(TestSetup testSetup) async { } String generateStringWithUuid(String prefix) { - return "$prefix-${uuidGenerator.v4()}"; + return "$prefix-${uuidGenerator.v4()}-dart-int-test"; +} + +Future verifyListFetch(String cacheName, String listName, + List expected, CacheClient cacheClient) async { + final listResp = await cacheClient.listFetch(cacheName, listName); + switch (listResp) { + case ListFetchHit(): + expect( + listResp.values.toList(), expected.map((e) => e.toUtf8()).toList()); + break; + case ListFetchMiss(): + fail('Expected Hit but got Miss'); + case ListFetchError(): + fail( + 'Expected Hit but got Error: ${listResp.errorCode} ${listResp.message}'); + } +} + +Future verifyListLength(String cacheName, String listName, int expected, + CacheClient cacheClient) async { + final listResp = await cacheClient.listLength(cacheName, listName); + switch (listResp) { + case ListLengthHit(): + expect(listResp.length, expected); + break; + case ListLengthMiss(): + fail('Expected Hit but got Miss'); + case ListLengthError(): + fail( + 'Expected Hit but got Error: ${listResp.errorCode} ${listResp.message}'); + } }