momento 0.2.0 → 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.rubocop.yml +14 -1
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +91 -0
  6. data/CONTRIBUTING.md +5 -6
  7. data/Gemfile +1 -6
  8. data/Gemfile.lock +30 -24
  9. data/README.md +53 -43
  10. data/README.template.md +30 -25
  11. data/examples/Gemfile +1 -1
  12. data/examples/README.md +3 -3
  13. data/examples/compact.rb +12 -8
  14. data/examples/example.rb +13 -7
  15. data/examples/file.rb +6 -5
  16. data/lib/README-generating-pb.txt +1 -1
  17. data/lib/momento/auth/credential_provider.rb +78 -0
  18. data/lib/momento/cache_client.rb +457 -0
  19. data/lib/momento/collection_ttl.rb +79 -0
  20. data/lib/momento/config/configuration.rb +16 -0
  21. data/lib/momento/config/configurations.rb +17 -0
  22. data/lib/momento/config/transport/grpc_configuration.rb +25 -0
  23. data/lib/momento/config/transport/static_transport_strategy.rb +12 -0
  24. data/lib/momento/config/transport/transport_strategy.rb +16 -0
  25. data/lib/momento/error/types.rb +15 -0
  26. data/lib/momento/generated/README.md +16 -0
  27. data/lib/momento/generated/auth_pb.rb +52 -0
  28. data/lib/momento/generated/auth_services_pb.rb +27 -0
  29. data/lib/momento/generated/cacheclient_pb.rb +203 -0
  30. data/lib/momento/generated/cacheclient_services_pb.rb +90 -0
  31. data/lib/momento/generated/cacheping_pb.rb +38 -0
  32. data/lib/momento/generated/cacheping_services_pb.rb +23 -0
  33. data/lib/momento/generated/cachepubsub_pb.rb +48 -0
  34. data/lib/momento/generated/cachepubsub_services_pb.rb +56 -0
  35. data/lib/momento/generated/common_pb.rb +44 -0
  36. data/lib/momento/generated/controlclient_pb.rb +72 -0
  37. data/lib/momento/generated/controlclient_services_pb.rb +35 -0
  38. data/lib/momento/generated/extensions_pb.rb +35 -0
  39. data/lib/momento/generated/generate_protos.sh +47 -0
  40. data/lib/momento/generated/leaderboard_pb.rb +56 -0
  41. data/lib/momento/generated/leaderboard_services_pb.rb +57 -0
  42. data/lib/momento/generated/permissionmessages_pb.rb +48 -0
  43. data/lib/momento/generated/token_pb.rb +43 -0
  44. data/lib/momento/generated/token_services_pb.rb +23 -0
  45. data/lib/momento/generated/webhook_pb.rb +49 -0
  46. data/lib/momento/generated/webhook_services_pb.rb +32 -0
  47. data/lib/momento/response/control/create_cache_response.rb +61 -0
  48. data/lib/momento/{delete_cache_response_builder.rb → response/control/delete_cache_response.rb} +24 -3
  49. data/lib/momento/response/control/list_caches_response.rb +80 -0
  50. data/lib/momento/{delete_response_builder.rb → response/delete_response.rb} +24 -2
  51. data/lib/momento/{get_response.rb → response/get_response.rb} +39 -9
  52. data/lib/momento/{response.rb → response/response.rb} +11 -14
  53. data/lib/momento/response/set_response.rb +59 -0
  54. data/lib/momento/response/sort_order.rb +11 -0
  55. data/lib/momento/response/sorted_set/sorted_set_fetch_response.rb +107 -0
  56. data/lib/momento/response/sorted_set/sorted_set_put_element_response.rb +44 -0
  57. data/lib/momento/response/sorted_set/sorted_set_put_elements_response.rb +44 -0
  58. data/lib/momento/version.rb +1 -1
  59. data/lib/momento.rb +6 -1
  60. data/momento.gemspec +5 -3
  61. data/release-please-config.json +1 -1
  62. data/sig/momento/auth/credential_provider.rbs +11 -0
  63. data/sig/momento/cache_client.rbs +12 -0
  64. data/sig/momento/collection_ttl.rbs +22 -0
  65. data/sig/momento/config/configuration.rbs +9 -0
  66. data/sig/momento/config/configurations.rbs +9 -0
  67. data/sig/momento/config/transport/grpc_configuration.rbs +7 -0
  68. data/sig/momento/config/transport/transport_strategy.rbs +7 -0
  69. data/sig/momento/list_caches_response.rbs +7 -0
  70. data/sig/momento/sorted_set_fetch_response.rbs +13 -0
  71. data/sig/momento/sorted_set_put_element_response.rbs +5 -0
  72. data/sig/momento/sorted_set_put_elements_response.rbs +5 -0
  73. metadata +101 -40
  74. data/lib/momento/cacheclient_pb.rb +0 -334
  75. data/lib/momento/cacheclient_services_pb.rb +0 -44
  76. data/lib/momento/controlclient_pb.rb +0 -73
  77. data/lib/momento/controlclient_services_pb.rb +0 -31
  78. data/lib/momento/create_cache_response.rb +0 -37
  79. data/lib/momento/create_cache_response_builder.rb +0 -27
  80. data/lib/momento/delete_cache_response.rb +0 -24
  81. data/lib/momento/delete_response.rb +0 -24
  82. data/lib/momento/get_response_builder.rb +0 -37
  83. data/lib/momento/list_caches_response.rb +0 -77
  84. data/lib/momento/list_caches_response_builder.rb +0 -25
  85. data/lib/momento/set_response.rb +0 -39
  86. data/lib/momento/set_response_builder.rb +0 -25
  87. data/lib/momento/simple_cache_client.rb +0 -336
  88. /data/lib/momento/{response_builder.rb → response/response_builder.rb} +0 -0
data/examples/file.rb CHANGED
@@ -5,9 +5,6 @@
5
5
 
6
6
  require 'momento'
7
7
 
8
- # Get your Momento token from an environment variable.
9
- TOKEN = ENV.fetch('MOMENTO_AUTH_TOKEN')
10
-
11
8
  # Cached items will be deleted after 12.5 seconds.
12
9
  TTL_SECONDS = 12.5
13
10
 
@@ -21,9 +18,13 @@ FILE_LOCATIONS = [
21
18
  "../spec/support/assets/test.jpg"
22
19
  ].freeze
23
20
 
21
+ # Create a credential provider that loads a Momento API Key from an environment variable.
22
+ credential_provider = Momento::CredentialProvider.from_env_var('MOMENTO_API_KEY')
23
+
24
24
  # Instantiate a Momento client.
25
- client = Momento::SimpleCacheClient.new(
26
- auth_token: TOKEN,
25
+ client = Momento::CacheClient.new(
26
+ configuration: Momento::Cache::Configurations::Laptop.latest,
27
+ credential_provider: credential_provider,
27
28
  default_ttl: TTL_SECONDS
28
29
  )
29
30
 
@@ -25,4 +25,4 @@ Put them in their own namespace.
25
25
  1. Wrap the modules in `module Momento`.
26
26
  2. In the *_services_pb.rb files, change the module names.
27
27
  * rename ::ControlClient to ::Momento::ControlClient
28
- * rename ::CacheClient to ::Momento::CacheClient
28
+ * rename ::CacheClient to ::MomentoProtos::CacheClient
@@ -0,0 +1,78 @@
1
+ require "base64"
2
+ require "json"
3
+
4
+ module Momento
5
+ # Contains the information required for a Momento client to connect to and authenticate with Momento services.
6
+ class CredentialProvider
7
+ attr_reader :api_key, :control_endpoint, :cache_endpoint
8
+
9
+ # Creates a CredentialProvider from a Momento API key loaded from an environment variable.
10
+ # @param env_var_name [String] the environment variable containing the API key
11
+ # @return [Momento::CredentialProvider]
12
+ # @raise [Momento::Error::InvalidArgumentError] if the API key is invalid
13
+ def self.from_env_var(env_var_name)
14
+ api_key = ENV.fetch(env_var_name) {
15
+ raise Momento::Error::InvalidArgumentError, "Env var #{env_var_name} must be set"
16
+ }
17
+ new(api_key)
18
+ end
19
+
20
+ # Creates a CredentialProvider from a Momento API key
21
+ # @param api_key [String] the Momento API key
22
+ # @return [Momento::CredentialProvider]
23
+ # @raise [Momento::Error::InvalidArgumentError] if the API key is invalid
24
+ def self.from_string(api_key)
25
+ raise Momento::Error::InvalidArgumentError, 'Auth token string cannot be empty' if api_key.empty?
26
+
27
+ new(api_key)
28
+ end
29
+
30
+ private
31
+
32
+ def initialize(api_key)
33
+ decoded_token = decode_api_key(api_key)
34
+ @api_key = decoded_token.api_key
35
+ @control_endpoint = decoded_token.control_endpoint
36
+ @cache_endpoint = decoded_token.cache_endpoint
37
+ rescue StandardError => e
38
+ raise Momento::Error::InvalidArgumentError, e.message
39
+ end
40
+
41
+ AuthTokenData = Struct.new(:api_key, :cache_endpoint, :control_endpoint)
42
+
43
+ def decode_api_key(api_key)
44
+ decode_v1_key(api_key)
45
+ rescue StandardError
46
+ decode_legacy_key(api_key)
47
+ end
48
+
49
+ def decode_legacy_key(api_key)
50
+ key_parts = api_key.split('.')
51
+ raise Momento::Error::InvalidArgumentError, 'Malformed legacy API key' if key_parts.size != 3
52
+
53
+ decoded_key = Base64.decode64(key_parts[1])
54
+ key_json = JSON.parse(decoded_key, symbolize_names: true)
55
+ validate_key_json(key_json, %i[c cp])
56
+
57
+ AuthTokenData.new(api_key, key_json[:c], key_json[:cp])
58
+ end
59
+
60
+ def decode_v1_key(api_key)
61
+ decoded_key = Base64.decode64(api_key)
62
+ key_json = JSON.parse(decoded_key, symbolize_names: true)
63
+ validate_key_json(key_json, %i[api_key endpoint])
64
+
65
+ AuthTokenData.new(key_json[:api_key], "cache.#{key_json[:endpoint]}", "control.#{key_json[:endpoint]}")
66
+ rescue StandardError
67
+ raise Momento::Error::InvalidArgumentError, 'Malformed Momento API Key'
68
+ end
69
+
70
+ def validate_key_json(key_json, required_fields)
71
+ missing_fields = required_fields.reject { |field| key_json.key?(field) }
72
+ return if missing_fields.empty?
73
+
74
+ raise Momento::Error::InvalidArgumentError,
75
+ "Required fields are missing: #{missing_fields.join(', ')}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,457 @@
1
+ require_relative 'generated/cacheclient_services_pb'
2
+ require_relative 'generated/controlclient_services_pb'
3
+ require_relative 'response/response'
4
+ require_relative 'ttl'
5
+ require_relative 'exceptions'
6
+
7
+ module Momento
8
+ # rubocop:disable Metrics/ClassLength
9
+
10
+ # A simple client for Momento.
11
+ #
12
+ # CacheClient does not use exceptions to report errors.
13
+ # Instead it returns an error response. Please see {file:README.md#label-Error+Handling}.
14
+ #
15
+ # @example
16
+ # credential_provider = Momento::CredentialProvider.from_env_var('MOMENTO_API_KEY')
17
+ # config = Momento::Cache::Configurations::Laptop.latest
18
+ # client = Momento::CacheClient.new(
19
+ # configuration: config,
20
+ # credential_provider: credential_provider,
21
+ # # cached items will be deleted after 100 seconds
22
+ # default_ttl: 100
23
+ # )
24
+ #
25
+ # response = client.create_cache("my_cache")
26
+ # if response.success?
27
+ # puts "my_cache was created"
28
+ # elsif response.already_exists?
29
+ # puts "my_cache already exists"
30
+ # elsif response.error?
31
+ # raise response.error
32
+ # end
33
+ #
34
+ # # set will only return success or error,
35
+ # # we only need to check for error
36
+ # response = client.set("my_cache", "key", "value")
37
+ # raise response.error if response.error?
38
+ #
39
+ # response = client.get("my_cache", "key")
40
+ # if response.hit?
41
+ # puts "We got #{response.value_string}"
42
+ # elsif response.miss?
43
+ # puts "It's not in the cache"
44
+ # elsif response.error?
45
+ # raise response.error
46
+ # end
47
+ #
48
+ # @see Momento::Response
49
+ class CacheClient
50
+ # This gem's version.
51
+ VERSION = Momento::VERSION
52
+ CACHE_CLIENT_STUB_CLASS = MomentoProtos::CacheClient::Scs::Stub
53
+ CONTROL_CLIENT_STUB_CLASS = MomentoProtos::ControlClient::ScsControl::Stub
54
+ private_constant :CACHE_CLIENT_STUB_CLASS, :CONTROL_CLIENT_STUB_CLASS
55
+
56
+ # @return [Numeric] how long items should remain in the cache, in seconds.
57
+ attr_accessor :default_ttl
58
+
59
+ # @param configuration [Momento::Cache::Configuration] the configuration for the client
60
+ # @param credential_provider [Momento::CredentialProvider] the provider for the
61
+ # credentials required to connect to Momento
62
+ # @param default_ttl [Numeric] time-to-live, in seconds
63
+ # @raise [ArgumentError] if the default_ttl or credential_provider is invalid
64
+ def initialize(configuration:, credential_provider:, default_ttl:)
65
+ @default_ttl = Momento::Ttl.to_ttl(default_ttl)
66
+ @api_key = credential_provider.api_key
67
+ @control_endpoint = credential_provider.control_endpoint
68
+ @cache_endpoint = credential_provider.cache_endpoint
69
+ @configuration = configuration
70
+ end
71
+
72
+ # Get a value in a cache.
73
+ #
74
+ # The value can be retrieved as either bytes or a string.
75
+ # @example
76
+ # response = client.get("my_cache", "key")
77
+ # if response.hit?
78
+ # puts "We got #{response.value_string}"
79
+ # elsif response.miss?
80
+ # puts "It's not in the cache"
81
+ # elsif response.error?
82
+ # raise response.error
83
+ # end
84
+ #
85
+ # @see Momento::GetResponse
86
+ # @param cache_name [String]
87
+ # @param key [String] must only contain ASCII characters
88
+ # @return [Momento::GetResponse]
89
+ # @raise [TypeError] when the cache_name or key is not a String
90
+ def get(cache_name, key)
91
+ builder = GetResponseBuilder.new(
92
+ context: { cache_name: cache_name, key: key }
93
+ )
94
+
95
+ builder.from_block do
96
+ cache_stub.get(
97
+ MomentoProtos::CacheClient::PB__GetRequest.new(cache_key: to_bytes(key)),
98
+ metadata: { cache: validate_cache_name(cache_name) }
99
+ )
100
+ end
101
+ end
102
+
103
+ # Set a value in a cache.
104
+ #
105
+ # If ttl is not set, it will use the default_ttl.
106
+ # @example
107
+ # response = client.set("my_cache", "key", "value")
108
+ # raise response.error if response.error?
109
+ #
110
+ # @see Momento::SetResponse
111
+ # @param cache_name [String]
112
+ # @param key [String] must only contain ASCII characters
113
+ # @param value [String] the value to cache
114
+ # @param ttl [Numeric] time-to-live, in seconds.
115
+ # @raise [ArgumentError] if the ttl is invalid
116
+ # @return [Momento::SetResponse]
117
+ # @raise [TypeError] when the cache_name, key, or value is not a String
118
+ def set(cache_name, key, value, ttl: default_ttl)
119
+ ttl = Momento::Ttl.to_ttl(ttl)
120
+
121
+ builder = SetResponseBuilder.new(
122
+ context: { cache_name: cache_name, key: key, value: value, ttl: ttl }
123
+ )
124
+
125
+ builder.from_block do
126
+ req = MomentoProtos::CacheClient::PB__SetRequest.new(
127
+ cache_key: to_bytes(key),
128
+ cache_body: to_bytes(value),
129
+ ttl_milliseconds: ttl.milliseconds
130
+ )
131
+
132
+ cache_stub.set(req, metadata: { cache: validate_cache_name(cache_name) })
133
+ end
134
+ end
135
+
136
+ # Delete a key in a cache.
137
+ #
138
+ # If the key does not exist, delete will still succeed.
139
+ # @example
140
+ # response = client.delete("my_cache", "key")
141
+ # raise response.error if response.error?
142
+ #
143
+ # @see Momento::DeleteResponse
144
+ # @param cache_name [String]
145
+ # @param key [String] must only contain ASCII characters
146
+ # @return [Momento::DeleteResponse]
147
+ # @raise [TypeError] when the cache_name or key is not a String
148
+ def delete(cache_name, key)
149
+ builder = DeleteResponseBuilder.new(
150
+ context: { cache_name: cache_name, key: key }
151
+ )
152
+
153
+ builder.from_block do
154
+ cache_stub.delete(
155
+ MomentoProtos::CacheClient::PB__DeleteRequest.new(cache_key: to_bytes(key)),
156
+ metadata: { cache: validate_cache_name(cache_name) }
157
+ )
158
+ end
159
+ end
160
+
161
+ # Create a new Momento cache.
162
+ # @example
163
+ # response = client.create_cache("my_cache")
164
+ # if response.success?
165
+ # puts "my_cache was created"
166
+ # elsif response.already_exists?
167
+ # puts "my_cache already exists"
168
+ # elsif response.error?
169
+ # raise response.error
170
+ # end
171
+ #
172
+ # @see Momento::CreateCacheResponse
173
+ # @param cache_name [String] the name of the cache to create.
174
+ # @return [Momento::CreateCacheResponse] the response from Momento.
175
+ # @raise [TypeError] when the cache_name is not a String
176
+ def create_cache(cache_name)
177
+ builder = CreateCacheResponseBuilder.new(
178
+ context: { cache_name: cache_name }
179
+ )
180
+
181
+ builder.from_block do
182
+ control_stub.create_cache(
183
+ MomentoProtos::ControlClient::PB__CreateCacheRequest.new(cache_name: validate_cache_name(cache_name))
184
+ )
185
+ end
186
+ end
187
+
188
+ # Delete an existing Momento cache.
189
+ #
190
+ # @example
191
+ # response = client.delete_cache("my_cache")
192
+ # raise response.error if response.error?
193
+ #
194
+ # @see Momento::DeleteCacheResponse
195
+ # @param cache_name [String] the name of the cache to delete.
196
+ # @return [Momento::DeleteCacheResponse] the response from Momento.
197
+ # @raise [TypeError] when the cache_name is not a String
198
+ def delete_cache(cache_name)
199
+ builder = DeleteCacheResponseBuilder.new(
200
+ context: { cache_name: cache_name }
201
+ )
202
+
203
+ builder.from_block do
204
+ control_stub.delete_cache(
205
+ MomentoProtos::ControlClient::PB__DeleteCacheRequest.new(cache_name: validate_cache_name(cache_name))
206
+ )
207
+ end
208
+ end
209
+
210
+ # Lists your caches.
211
+ #
212
+ # @see Momento::ListCachesResponse
213
+ # @return [Momento::ListCachesResponse]
214
+ def list_caches
215
+ builder = ListCachesResponseBuilder.new(
216
+ context: {}
217
+ )
218
+ builder.from_block do
219
+ control_stub.list_caches(
220
+ MomentoProtos::ControlClient::PB__ListCachesRequest.new
221
+ )
222
+ end
223
+ end
224
+
225
+ # Put an element in a sorted set
226
+ #
227
+ # If collection_ttl is not set, it will use the default_ttl.
228
+ # @example
229
+ # response = client.sorted_set_put_element('my_cache', 'my_set', 'value', 1.0)
230
+ # raise response.error if response.error?
231
+ #
232
+ # @see Momento::SortedSetPutElementResponse
233
+ # @param cache_name [String]
234
+ # @param sorted_set_name [String]
235
+ # @param value [String] the value to add to the sorted set.
236
+ # @param score [Float] the score of the value. Determines its place in the set.
237
+ # @param collection_ttl [Momento::CollectionTtl] time-to-live, in seconds.
238
+ # @raise [ArgumentError] if the ttl is invalid
239
+ # @return [Momento::SortedSetPutElementResponse]
240
+ # @raise [TypeError] when the cache_name, sorted_set_name, or value is not a String
241
+ def sorted_set_put_element(cache_name, sorted_set_name, value, score, collection_ttl: CollectionTtl.from_cache_ttl)
242
+ collection_ttl = collection_ttl.with_ttl_if_absent(default_ttl.seconds)
243
+ builder = SortedSetPutElementResponseBuilder.new(
244
+ context: { cache_name: cache_name, set_name: sorted_set_name, value: value, score: score,
245
+ collection_ttl: collection_ttl }
246
+ )
247
+
248
+ builder.from_block do
249
+ req = MomentoProtos::CacheClient::PB__SortedSetPutRequest.new(
250
+ set_name: to_bytes(sorted_set_name),
251
+ elements: [{ value: to_bytes(value), score: score }],
252
+ ttl_milliseconds: collection_ttl.ttl_milliseconds,
253
+ refresh_ttl: collection_ttl.refresh_ttl
254
+ )
255
+
256
+ # noinspection RubyResolve
257
+ cache_stub.sorted_set_put(req, metadata: { cache: validate_cache_name(cache_name) })
258
+ end
259
+ end
260
+
261
+ # Put multiple elements in a sorted set
262
+ #
263
+ # If collection_ttl is not set, it will use the default_ttl.
264
+ # @example
265
+ # response = client.sorted_set_put_element('my_cache', 'my_set', [['value', 1.0]])
266
+ # raise response.error if response.error?
267
+ #
268
+ # @see Momento::SortedSetPutElementsResponse
269
+ # @param cache_name [String]
270
+ # @param sorted_set_name [String]
271
+ # @param elements [Hash, Array] the elements to add. Must be a hash of String values to Float scores,
272
+ # an array of arrays [["value", 1.0]], or an array of hashes of value and score [{value: "value", score: 1.0}].
273
+ # @param collection_ttl [Integer] time-to-live, in seconds.
274
+ # @raise [ArgumentError] if the ttl is invalid
275
+ # @return [Momento::SortedSetPutElementsResponse]
276
+ # @raise [TypeError] when the cache_name, or sorted_set_name is not a String, or if elements is not
277
+ # an Array or Hash
278
+ def sorted_set_put_elements(cache_name, sorted_set_name, elements, collection_ttl = CollectionTtl.from_cache_ttl)
279
+ collection_ttl = collection_ttl.with_ttl_if_absent(default_ttl.seconds)
280
+ builder = SortedSetPutElementsResponseBuilder.new(
281
+ context: { cache_name: cache_name, set_name: sorted_set_name, elements: elements,
282
+ collection_ttl: collection_ttl }
283
+ )
284
+
285
+ builder.from_block do
286
+ req = MomentoProtos::CacheClient::PB__SortedSetPutRequest.new(
287
+ set_name: to_bytes(sorted_set_name),
288
+ elements: to_sorted_set_elements(elements),
289
+ ttl_milliseconds: collection_ttl.ttl_milliseconds,
290
+ refresh_ttl: collection_ttl.refresh_ttl
291
+ )
292
+
293
+ # noinspection RubyResolve
294
+ cache_stub.sorted_set_put(req, metadata: { cache: validate_cache_name(cache_name) })
295
+ end
296
+ end
297
+
298
+ # rubocop:disable Metrics/ParameterLists
299
+
300
+ # Fetch the elements a sorted set by score.
301
+ #
302
+ # @example
303
+ # response = client.sorted_set_fetch_by_score("my_cache", "sorted_set", min_score: 0.0, max_score: 1.0)
304
+ # raise response.error if response.error?
305
+ #
306
+ # @see Momento::SortedSetFetchResponse
307
+ # @param cache_name [String]
308
+ # @param sorted_set_name [String]
309
+ # @param min_score [Float] The minimum score (inclusive) of the elements to fetch. Defaults to negative infinity.
310
+ # @param max_score [Float] The maximum score (inclusive) of the elements to fetch. Defaults to positive infinity.
311
+ # @param sort_order [SortOrder] The order to fetch the elements in. Defaults to ascending.
312
+ # @param offset [Integer] The number of elements to skip before returning the first element. Defaults to 0.
313
+ # @param count [Integer] The maximum number of elements to return. Defaults to all elements.
314
+ # @return [Momento::SortedSetFetchResponse]
315
+ # @raise [TypeError] when the cache_name, or sorted_set_name is not a String.
316
+ def sorted_set_fetch_by_score(cache_name, sorted_set_name, min_score: nil, max_score: nil,
317
+ sort_order: SortOrder::ASCENDING, offset: 0, count: -1)
318
+ builder = SortedSetFetchResponseBuilder.new(
319
+ context: { cache_name: cache_name, set_name: sorted_set_name, min_score: min_score, max_score: max_score,
320
+ sort_order: sort_order, offset: offset, count: count }
321
+ )
322
+
323
+ builder.from_block do
324
+ by_score = build_sorted_set_by_score(min_score, max_score, offset, count)
325
+
326
+ req = MomentoProtos::CacheClient::PB__SortedSetFetchRequest.new(
327
+ set_name: to_bytes(sorted_set_name),
328
+ order: to_grpc_order(sort_order),
329
+ with_scores: true,
330
+ by_score: by_score
331
+ )
332
+
333
+ # noinspection RubyResolve
334
+ cache_stub.sorted_set_fetch(req, metadata: { cache: validate_cache_name(cache_name) })
335
+ end
336
+ end
337
+ # rubocop:enable Metrics/ParameterLists
338
+
339
+ private
340
+
341
+ def cache_stub
342
+ @cache_stub ||= CACHE_CLIENT_STUB_CLASS.new(@cache_endpoint, combined_credentials,
343
+ timeout: @configuration.transport_strategy.grpc_configuration.deadline
344
+ )
345
+ end
346
+
347
+ def control_stub
348
+ @control_stub ||= CONTROL_CLIENT_STUB_CLASS.new(@control_endpoint, combined_credentials)
349
+ end
350
+
351
+ def combined_credentials
352
+ @combined_credentials ||= make_combined_credentials
353
+ end
354
+
355
+ def make_combined_credentials
356
+ # :nocov:
357
+ auth_proc = proc do
358
+ { authorization: @api_key, agent: "ruby:#{VERSION}" }
359
+ end
360
+ # :nocov:
361
+
362
+ call_creds = GRPC::Core::CallCredentials.new(auth_proc)
363
+
364
+ GRPC::Core::ChannelCredentials.new.compose(call_creds)
365
+ end
366
+
367
+ def to_grpc_order(sort_order)
368
+ case sort_order
369
+ when SortOrder::ASCENDING
370
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::Order::ASCENDING
371
+ when SortOrder::DESCENDING
372
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::Order::DESCENDING
373
+ else
374
+ raise TypeError, "Invalid sort order: #{sort_order}"
375
+ end
376
+ end
377
+
378
+ # Momento accepts sorted sets as an array of hashes. This will transform an array of arrays [["value", 1.0]],
379
+ # an array of hashes [{value: "value", score: 1.0}], or a hash of values to scores to the correct format.
380
+ # @param elements [Hash, Array] A hash of string values to scores or an array of tuples (value, score)
381
+ # # @return [Array<Hash>] An array of sorted set elements, where each element is a hash of :value and :score
382
+ def to_sorted_set_elements(elements)
383
+ case elements
384
+ when Hash
385
+ elements.map { |value, score| { value: to_bytes(value), score: score.to_f } }
386
+ when Array
387
+ if elements.first.is_a?(Hash)
388
+ elements.map { |element| { value: to_bytes(element[:value]), score: element[:score].to_f } }
389
+ else
390
+ elements.map { |value, score| { value: to_bytes(value), score: score.to_f } }
391
+ end
392
+ else
393
+ raise ArgumentError, "Sorted set elements must be a Hash or an Array of tuples"
394
+ end
395
+ end
396
+
397
+ def build_sorted_set_by_score(min_score, max_score, offset, count)
398
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::PB__ByScore.new(
399
+ min_score: min_score ? build_score(min_score) : nil,
400
+ unbounded_min: min_score ? nil : MomentoProtos::Common::PB__Unbounded.new,
401
+ max_score: max_score ? build_score(max_score) : nil,
402
+ unbounded_max: max_score ? nil : MomentoProtos::Common::PB__Unbounded.new,
403
+ offset: offset,
404
+ count: count
405
+ )
406
+ end
407
+
408
+ def build_score(score)
409
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::PB__ByScore::PB__Score.new(
410
+ score: score,
411
+ exclusive: false
412
+ )
413
+ end
414
+
415
+ # Ruby uses String for bytes. GRPC wants a String encoded as ASCII.
416
+ # GRPC will re-encode a String, but treats it as characters; GRPC will
417
+ # raise if you pass a String with non-ASCII characters.
418
+ # So we do the re-encoding ourselves in a way that treats the String as
419
+ # bytes and will not raise. The data is not changed.
420
+ #
421
+ # If the input String is ASCII, we treat it as binary data. Otherwise,
422
+ # we ensure it is encoded as UTF-8 to stop the SDK from being able to
423
+ # write non-UTF-8 strings to the server.
424
+ #
425
+ # A duplicate String is returned, but since Ruby is copy-on-write it
426
+ # does not copy the data.
427
+ #
428
+ # @param string [String] the string to make safe for GRPC bytes
429
+ # @return [String] a duplicate safe to use as GRPC bytes
430
+ # @raise [TypeError] when the string is not a String
431
+ def to_bytes(string)
432
+ raise TypeError, "expected a String, got a #{string.class}" unless string.is_a?(String)
433
+
434
+ if string.encoding == Encoding::ASCII_8BIT
435
+ string.dup
436
+ else
437
+ utf8_encoded = string.encode('UTF-8')
438
+ utf8_encoded.force_encoding(Encoding::ASCII_8BIT)
439
+ end
440
+ end
441
+
442
+ # Return a UTF-8 version of the cache name.
443
+ #
444
+ # @param name [String] the cache name to validate
445
+ # @raise [TypeError] when the name is not a String
446
+ # @raise [Momento::CacheNameError] when the name is not UTF-8 compatible
447
+ def validate_cache_name(name)
448
+ raise TypeError, "Cache name must be a String, got a #{name.class}" unless name.is_a?(String)
449
+
450
+ encoded_name = name.encode('UTF-8')
451
+ raise Momento::CacheNameError, "Cache name must be UTF-8 compatible" unless name.valid_encoding?
452
+
453
+ encoded_name
454
+ end
455
+ end
456
+ # rubocop:enable Metrics/ClassLength
457
+ end
@@ -0,0 +1,79 @@
1
+ module Momento
2
+ # Represents the desired behavior for managing the TTL on collection objects.
3
+ #
4
+ # For cache operations that modify a collection (dictionaries, lists, or sets), there
5
+ # are a few things to consider. The first time the collection is created, we need to
6
+ # set a TTL on it. For subsequent operations that modify the collection you may choose
7
+ # to update the TTL in order to prolong the life of the cached collection object, or
8
+ # you may choose to leave the TTL unmodified in order to ensure that the collection
9
+ # expires at the original TTL.
10
+ #
11
+ # The default behaviour is to refresh the TTL (to prolong the life of the collection)
12
+ # each time it is written using the client's default item TTL.
13
+ class CollectionTtl
14
+ attr_reader :ttl_seconds, :refresh_ttl
15
+
16
+ # Create a CollectionTtl with optional ttl seconds and refresh.
17
+ # @param ttl_seconds [Integer | nil] the time to live of the collection. Uses the client default TTL if nil.
18
+ # @param refresh_ttl [Boolean] whether to refresh the collection's ttl when performing a cache operation.
19
+ # @return [Momento::CollectionTtl]
20
+ def initialize(ttl_seconds = nil, refresh_ttl: true)
21
+ validate_ttl_seconds(ttl_seconds) unless ttl_seconds.nil?
22
+ @ttl_seconds = ttl_seconds
23
+ @refresh_ttl = refresh_ttl
24
+ end
25
+
26
+ def ttl_milliseconds
27
+ @ttl_seconds.nil? ? nil : @ttl_seconds * 1000
28
+ end
29
+
30
+ # Creates a CollectionTtl that refreshes and uses the default client TTL.
31
+ # @return [Momento::CollectionTtl]
32
+ def self.from_cache_ttl
33
+ new
34
+ end
35
+
36
+ # Creates a CollectionTtl with the given TTL.
37
+ # @param ttl_seconds [Integer | nil] the time to live of the collection. Uses the client default TTL if nil.
38
+ # @return [Momento::CollectionTtl]
39
+ def self.of(ttl_seconds)
40
+ new(ttl_seconds)
41
+ end
42
+
43
+ # Creates a CollectionTtl that sets refresh to true if ttl_seconds is provided and false otherwise
44
+ # @param ttl_seconds [Integer | nil] the time to live of the collection. If not nil, refresh is set to true.
45
+ # @return [Momento::CollectionTtl]
46
+ def self.refresh_ttl_if_provided(ttl_seconds = nil)
47
+ new(ttl_seconds, refresh_ttl: !ttl_seconds.nil?)
48
+ end
49
+
50
+ # Copy constructor that uses the given TTL only if the parent CollectionTtl doesn't have one.
51
+ # @param ttl_seconds [Integer | nil] the time to live of the collection. Will be ignored if the parent has a TTL.
52
+ # @return [Momento::CollectionTtl]
53
+ def with_ttl_if_absent(ttl_seconds)
54
+ self.class.new(@ttl_seconds || ttl_seconds, refresh_ttl: @refresh_ttl)
55
+ end
56
+
57
+ # Copy constructor that uses the parent TTL and refreshes.
58
+ # @return [Momento::CollectionTtl]
59
+ def with_refresh_ttl_on_updates
60
+ self.class.new(@ttl_seconds)
61
+ end
62
+
63
+ # Copy constructor that uses the parent TTL and does not refresh.
64
+ # @return [Momento::CollectionTtl]
65
+ def with_no_refresh_ttl_on_updates
66
+ self.class.new(@ttl_seconds, refresh_ttl: false)
67
+ end
68
+
69
+ def to_s
70
+ "ttl: #{@ttl_seconds || 'null'}, refreshTtl: #{@refresh_ttl ? 'true' : 'false'}"
71
+ end
72
+
73
+ private
74
+
75
+ def validate_ttl_seconds(ttl_seconds)
76
+ raise ArgumentError, "TTL must be a positive integer" unless ttl_seconds.is_a?(Integer) && ttl_seconds.positive?
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ module Momento
2
+ module Cache
3
+ # Configuration options for Momento CacheClient
4
+ class Configuration
5
+ attr_reader :transport_strategy
6
+
7
+ def self.with_transport_strategy(transport_strategy)
8
+ return Configuration.new(transport_strategy)
9
+ end
10
+
11
+ def initialize(transport_strategy)
12
+ @transport_strategy = transport_strategy
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'configuration'
2
+ require_relative 'transport/transport_strategy'
3
+ require_relative 'transport/static_transport_strategy'
4
+ require_relative 'transport/grpc_configuration'
5
+
6
+ module Momento
7
+ module Cache
8
+ module Configurations
9
+ # Default Laptop configuration with 5000ms client timeout
10
+ class Laptop < Cache::Configuration
11
+ def self.latest
12
+ return Configuration.new(StaticTransportStrategy.new(GrpcConfiguration.new(5000)))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end