momento 0.2.0 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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