momento 0.2.0 → 0.5.0

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 (89) 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 +105 -0
  6. data/CONTRIBUTING.md +5 -6
  7. data/Gemfile +1 -6
  8. data/Gemfile.lock +34 -28
  9. data/README.md +64 -44
  10. data/README.template.md +30 -25
  11. data/examples/.gitignore +1 -0
  12. data/examples/Gemfile +1 -1
  13. data/examples/README.md +5 -6
  14. data/examples/compact.rb +13 -9
  15. data/examples/example.rb +24 -8
  16. data/examples/file.rb +7 -6
  17. data/lib/README-generating-pb.txt +1 -1
  18. data/lib/momento/auth/credential_provider.rb +78 -0
  19. data/lib/momento/cache_client.rb +478 -0
  20. data/lib/momento/collection_ttl.rb +79 -0
  21. data/lib/momento/config/configuration.rb +42 -0
  22. data/lib/momento/config/configurations.rb +24 -0
  23. data/lib/momento/config/transport/grpc_configuration.rb +35 -0
  24. data/lib/momento/config/transport/static_transport_strategy.rb +12 -0
  25. data/lib/momento/config/transport/transport_strategy.rb +16 -0
  26. data/lib/momento/error/types.rb +22 -0
  27. data/lib/momento/generated/README.md +16 -0
  28. data/lib/momento/generated/auth_pb.rb +52 -0
  29. data/lib/momento/generated/auth_services_pb.rb +27 -0
  30. data/lib/momento/generated/cacheclient_pb.rb +203 -0
  31. data/lib/momento/generated/cacheclient_services_pb.rb +90 -0
  32. data/lib/momento/generated/cacheping_pb.rb +38 -0
  33. data/lib/momento/generated/cacheping_services_pb.rb +23 -0
  34. data/lib/momento/generated/cachepubsub_pb.rb +48 -0
  35. data/lib/momento/generated/cachepubsub_services_pb.rb +56 -0
  36. data/lib/momento/generated/common_pb.rb +44 -0
  37. data/lib/momento/generated/controlclient_pb.rb +72 -0
  38. data/lib/momento/generated/controlclient_services_pb.rb +35 -0
  39. data/lib/momento/generated/extensions_pb.rb +35 -0
  40. data/lib/momento/generated/generate_protos.sh +47 -0
  41. data/lib/momento/generated/leaderboard_pb.rb +56 -0
  42. data/lib/momento/generated/leaderboard_services_pb.rb +57 -0
  43. data/lib/momento/generated/permissionmessages_pb.rb +48 -0
  44. data/lib/momento/generated/token_pb.rb +43 -0
  45. data/lib/momento/generated/token_services_pb.rb +23 -0
  46. data/lib/momento/generated/webhook_pb.rb +49 -0
  47. data/lib/momento/generated/webhook_services_pb.rb +32 -0
  48. data/lib/momento/response/control/create_cache_response.rb +61 -0
  49. data/lib/momento/{delete_cache_response_builder.rb → response/control/delete_cache_response.rb} +24 -3
  50. data/lib/momento/response/control/list_caches_response.rb +80 -0
  51. data/lib/momento/{delete_response_builder.rb → response/delete_response.rb} +24 -2
  52. data/lib/momento/{get_response.rb → response/get_response.rb} +39 -9
  53. data/lib/momento/{response.rb → response/response.rb} +11 -14
  54. data/lib/momento/response/set_response.rb +59 -0
  55. data/lib/momento/response/sort_order.rb +11 -0
  56. data/lib/momento/response/sorted_set/sorted_set_fetch_response.rb +107 -0
  57. data/lib/momento/response/sorted_set/sorted_set_put_element_response.rb +44 -0
  58. data/lib/momento/response/sorted_set/sorted_set_put_elements_response.rb +44 -0
  59. data/lib/momento/version.rb +1 -1
  60. data/lib/momento.rb +6 -1
  61. data/momento.gemspec +5 -3
  62. data/release-please-config.json +1 -1
  63. data/sig/momento/auth/credential_provider.rbs +11 -0
  64. data/sig/momento/cache_client.rbs +11 -0
  65. data/sig/momento/collection_ttl.rbs +22 -0
  66. data/sig/momento/config/configuration.rbs +13 -0
  67. data/sig/momento/config/configurations.rbs +9 -0
  68. data/sig/momento/config/transport/grpc_configuration.rbs +9 -0
  69. data/sig/momento/config/transport/transport_strategy.rbs +7 -0
  70. data/sig/momento/list_caches_response.rbs +7 -0
  71. data/sig/momento/sorted_set_fetch_response.rbs +13 -0
  72. data/sig/momento/sorted_set_put_element_response.rbs +5 -0
  73. data/sig/momento/sorted_set_put_elements_response.rbs +5 -0
  74. metadata +101 -40
  75. data/lib/momento/cacheclient_pb.rb +0 -334
  76. data/lib/momento/cacheclient_services_pb.rb +0 -44
  77. data/lib/momento/controlclient_pb.rb +0 -73
  78. data/lib/momento/controlclient_services_pb.rb +0 -31
  79. data/lib/momento/create_cache_response.rb +0 -37
  80. data/lib/momento/create_cache_response_builder.rb +0 -27
  81. data/lib/momento/delete_cache_response.rb +0 -24
  82. data/lib/momento/delete_response.rb +0 -24
  83. data/lib/momento/get_response_builder.rb +0 -37
  84. data/lib/momento/list_caches_response.rb +0 -77
  85. data/lib/momento/list_caches_response_builder.rb +0 -25
  86. data/lib/momento/set_response.rb +0 -39
  87. data/lib/momento/set_response_builder.rb +0 -25
  88. data/lib/momento/simple_cache_client.rb +0 -336
  89. /data/lib/momento/{response_builder.rb → response/response_builder.rb} +0 -0
data/examples/compact.rb CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  require 'momento'
4
4
 
5
- # Get your Momento token from an environment variable.
6
- TOKEN = ENV.fetch('MOMENTO_AUTH_TOKEN')
7
-
8
- # Cached items will be deleted after 12.5 seconds.
9
- TTL_SECONDS = 12.5
5
+ # Cached items will be deleted after 10 seconds.
6
+ TTL_SECONDS = 10
10
7
 
11
8
  # The name of the cache to create *and delete*
12
- CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME')
9
+ CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME', 'ruby-examples')
10
+
11
+ # Create a credential provider that loads a Momento API Key from an environment variable.
12
+ credential_provider = Momento::CredentialProvider.from_env_var('MOMENTO_API_KEY')
13
13
 
14
14
  # Instantiate a Momento client.
15
- client = Momento::SimpleCacheClient.new(
16
- auth_token: TOKEN,
15
+ client = Momento::CacheClient.new(
16
+ configuration: Momento::Cache::Configurations::Laptop.latest,
17
+ credential_provider: credential_provider,
17
18
  default_ttl: TTL_SECONDS
18
19
  )
19
20
 
@@ -23,7 +24,10 @@ response = client.create_cache(CACHE_NAME)
23
24
  raise response.error if response.error?
24
25
 
25
26
  # List our caches.
26
- puts "Caches: #{client.caches.to_a.join(", ")}"
27
+ response = client.list_caches
28
+ raise response.error if response.error?
29
+
30
+ puts "Caches: #{response.cache_names&.join(", ")}"
27
31
 
28
32
  # Put an item in the cache.
29
33
  response = client.set(CACHE_NAME, "key", "You cached something!")
data/examples/example.rb CHANGED
@@ -1,20 +1,31 @@
1
1
  # An example of the basic functionality of
2
- # Momento::SimpleCacheClient.
2
+ # Momento::CacheClient.
3
3
 
4
4
  require 'momento'
5
5
 
6
- # Get your Momento token from an environment variable.
7
- TOKEN = ENV.fetch('MOMENTO_AUTH_TOKEN')
8
-
9
6
  # Cached items will be deleted after 12.5 seconds.
10
7
  TTL_SECONDS = 12.5
11
8
 
12
9
  # The name of the cache to create *and delete*
13
- CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME')
10
+ CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME', 'ruby-examples')
11
+
12
+ # Create a credential provider that loads a Momento API Key from an environment variable.
13
+ credential_provider = Momento::CredentialProvider.from_env_var('MOMENTO_API_KEY')
14
+
15
+ # This is a reasonable configuration for dev work on a laptop.
16
+ configuration = Momento::Cache::Configurations::Laptop.latest
17
+ # This configuration might be better for a production where you want more aggressive timeouts
18
+ # configuration = Momento::Cache::Configuration::InRegion.latest
19
+ # To set a custom timeout, you can use the with_timeout method.
20
+ # configuration = configuration.with_timeout(10_000)
21
+ # To increase the number of TCP connections for a client where you expect a high volume of traffic,
22
+ # you can use the with_num_connections method.
23
+ # configuration = configuration.with_num_connections(4)
14
24
 
15
25
  # Instantiate a Momento client.
16
- client = Momento::SimpleCacheClient.new(
17
- auth_token: TOKEN,
26
+ client = Momento::CacheClient.new(
27
+ configuration: configuration,
28
+ credential_provider: credential_provider,
18
29
  default_ttl: TTL_SECONDS
19
30
  )
20
31
 
@@ -29,7 +40,12 @@ elsif response.error?
29
40
  end
30
41
 
31
42
  # List our caches.
32
- puts "Caches: #{client.caches.to_a.join(", ")}"
43
+ response = client.list_caches
44
+ if response.success?
45
+ puts "Caches: #{response.cache_names&.join(", ")}"
46
+ elsif response.error?
47
+ raise "Couldn't list the caches: #{response.error}"
48
+ end
33
49
 
34
50
  # Put an item in the cache.
35
51
  response = client.set(CACHE_NAME, "key", "You cached something!")
data/examples/file.rb CHANGED
@@ -5,14 +5,11 @@
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
 
14
11
  # The name of the cache to create *and delete*
15
- CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME')
12
+ CACHE_NAME = ENV.fetch('MOMENTO_CACHE_NAME', 'ruby-examples')
16
13
 
17
14
  # So it can be run from the top of the repo
18
15
  # or from the examples directory.
@@ -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,478 @@
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
+ @next_cache_stub_index = 0
71
+ @num_cache_stubs = @configuration.transport_strategy.grpc_configuration.num_grpc_channels
72
+ @is_first_request = true
73
+ end
74
+
75
+ # Get a value in a cache.
76
+ #
77
+ # The value can be retrieved as either bytes or a string.
78
+ # @example
79
+ # response = client.get("my_cache", "key")
80
+ # if response.hit?
81
+ # puts "We got #{response.value_string}"
82
+ # elsif response.miss?
83
+ # puts "It's not in the cache"
84
+ # elsif response.error?
85
+ # raise response.error
86
+ # end
87
+ #
88
+ # @see Momento::GetResponse
89
+ # @param cache_name [String]
90
+ # @param key [String] must only contain ASCII characters
91
+ # @return [Momento::GetResponse]
92
+ # @raise [TypeError] when the cache_name or key is not a String
93
+ def get(cache_name, key)
94
+ builder = GetResponseBuilder.new(
95
+ context: { cache_name: cache_name, key: key }
96
+ )
97
+
98
+ builder.from_block do
99
+ cache_stub.get(
100
+ MomentoProtos::CacheClient::PB__GetRequest.new(cache_key: to_bytes(key)),
101
+ metadata: grpc_metadata(cache_name)
102
+ )
103
+ end
104
+ end
105
+
106
+ # Set a value in a cache.
107
+ #
108
+ # If ttl is not set, it will use the default_ttl.
109
+ # @example
110
+ # response = client.set("my_cache", "key", "value")
111
+ # raise response.error if response.error?
112
+ #
113
+ # @see Momento::SetResponse
114
+ # @param cache_name [String]
115
+ # @param key [String] must only contain ASCII characters
116
+ # @param value [String] the value to cache
117
+ # @param ttl [Numeric] time-to-live, in seconds.
118
+ # @raise [ArgumentError] if the ttl is invalid
119
+ # @return [Momento::SetResponse]
120
+ # @raise [TypeError] when the cache_name, key, or value is not a String
121
+ def set(cache_name, key, value, ttl: default_ttl)
122
+ ttl = Momento::Ttl.to_ttl(ttl)
123
+
124
+ builder = SetResponseBuilder.new(
125
+ context: { cache_name: cache_name, key: key, value: value, ttl: ttl }
126
+ )
127
+
128
+ builder.from_block do
129
+ req = MomentoProtos::CacheClient::PB__SetRequest.new(
130
+ cache_key: to_bytes(key),
131
+ cache_body: to_bytes(value),
132
+ ttl_milliseconds: ttl.milliseconds
133
+ )
134
+
135
+ cache_stub.set(req, metadata: grpc_metadata(cache_name))
136
+ end
137
+ end
138
+
139
+ # Delete a key in a cache.
140
+ #
141
+ # If the key does not exist, delete will still succeed.
142
+ # @example
143
+ # response = client.delete("my_cache", "key")
144
+ # raise response.error if response.error?
145
+ #
146
+ # @see Momento::DeleteResponse
147
+ # @param cache_name [String]
148
+ # @param key [String] must only contain ASCII characters
149
+ # @return [Momento::DeleteResponse]
150
+ # @raise [TypeError] when the cache_name or key is not a String
151
+ def delete(cache_name, key)
152
+ builder = DeleteResponseBuilder.new(
153
+ context: { cache_name: cache_name, key: key }
154
+ )
155
+
156
+ builder.from_block do
157
+ cache_stub.delete(
158
+ MomentoProtos::CacheClient::PB__DeleteRequest.new(cache_key: to_bytes(key)),
159
+ metadata: grpc_metadata(cache_name)
160
+ )
161
+ end
162
+ end
163
+
164
+ # Create a new Momento cache.
165
+ # @example
166
+ # response = client.create_cache("my_cache")
167
+ # if response.success?
168
+ # puts "my_cache was created"
169
+ # elsif response.already_exists?
170
+ # puts "my_cache already exists"
171
+ # elsif response.error?
172
+ # raise response.error
173
+ # end
174
+ #
175
+ # @see Momento::CreateCacheResponse
176
+ # @param cache_name [String] the name of the cache to create.
177
+ # @return [Momento::CreateCacheResponse] the response from Momento.
178
+ # @raise [TypeError] when the cache_name is not a String
179
+ def create_cache(cache_name)
180
+ builder = CreateCacheResponseBuilder.new(
181
+ context: { cache_name: cache_name }
182
+ )
183
+
184
+ builder.from_block do
185
+ control_stub.create_cache(
186
+ MomentoProtos::ControlClient::PB__CreateCacheRequest.new(cache_name: validate_cache_name(cache_name))
187
+ )
188
+ end
189
+ end
190
+
191
+ # Delete an existing Momento cache.
192
+ #
193
+ # @example
194
+ # response = client.delete_cache("my_cache")
195
+ # raise response.error if response.error?
196
+ #
197
+ # @see Momento::DeleteCacheResponse
198
+ # @param cache_name [String] the name of the cache to delete.
199
+ # @return [Momento::DeleteCacheResponse] the response from Momento.
200
+ # @raise [TypeError] when the cache_name is not a String
201
+ def delete_cache(cache_name)
202
+ builder = DeleteCacheResponseBuilder.new(
203
+ context: { cache_name: cache_name }
204
+ )
205
+
206
+ builder.from_block do
207
+ control_stub.delete_cache(
208
+ MomentoProtos::ControlClient::PB__DeleteCacheRequest.new(cache_name: validate_cache_name(cache_name))
209
+ )
210
+ end
211
+ end
212
+
213
+ # Lists your caches.
214
+ #
215
+ # @see Momento::ListCachesResponse
216
+ # @return [Momento::ListCachesResponse]
217
+ def list_caches
218
+ builder = ListCachesResponseBuilder.new(
219
+ context: {}
220
+ )
221
+ builder.from_block do
222
+ control_stub.list_caches(
223
+ MomentoProtos::ControlClient::PB__ListCachesRequest.new
224
+ )
225
+ end
226
+ end
227
+
228
+ # Put an element in a sorted set
229
+ #
230
+ # If collection_ttl is not set, it will use the default_ttl.
231
+ # @example
232
+ # response = client.sorted_set_put_element('my_cache', 'my_set', 'value', 1.0)
233
+ # raise response.error if response.error?
234
+ #
235
+ # @see Momento::SortedSetPutElementResponse
236
+ # @param cache_name [String]
237
+ # @param sorted_set_name [String]
238
+ # @param value [String] the value to add to the sorted set.
239
+ # @param score [Float] the score of the value. Determines its place in the set.
240
+ # @param collection_ttl [Momento::CollectionTtl] time-to-live, in seconds.
241
+ # @raise [ArgumentError] if the ttl is invalid
242
+ # @return [Momento::SortedSetPutElementResponse]
243
+ # @raise [TypeError] when the cache_name, sorted_set_name, or value is not a String
244
+ def sorted_set_put_element(cache_name, sorted_set_name, value, score, collection_ttl: CollectionTtl.from_cache_ttl)
245
+ collection_ttl = collection_ttl.with_ttl_if_absent(default_ttl.seconds)
246
+ builder = SortedSetPutElementResponseBuilder.new(
247
+ context: { cache_name: cache_name, set_name: sorted_set_name, value: value, score: score,
248
+ collection_ttl: collection_ttl }
249
+ )
250
+
251
+ builder.from_block do
252
+ req = MomentoProtos::CacheClient::PB__SortedSetPutRequest.new(
253
+ set_name: to_bytes(sorted_set_name),
254
+ elements: [{ value: to_bytes(value), score: score }],
255
+ ttl_milliseconds: collection_ttl.ttl_milliseconds,
256
+ refresh_ttl: collection_ttl.refresh_ttl
257
+ )
258
+
259
+ # noinspection RubyResolve
260
+ cache_stub.sorted_set_put(req, metadata: grpc_metadata(cache_name))
261
+ end
262
+ end
263
+
264
+ # Put multiple elements in a sorted set
265
+ #
266
+ # If collection_ttl is not set, it will use the default_ttl.
267
+ # @example
268
+ # response = client.sorted_set_put_element('my_cache', 'my_set', [['value', 1.0]])
269
+ # raise response.error if response.error?
270
+ #
271
+ # @see Momento::SortedSetPutElementsResponse
272
+ # @param cache_name [String]
273
+ # @param sorted_set_name [String]
274
+ # @param elements [Hash, Array] the elements to add. Must be a hash of String values to Float scores,
275
+ # an array of arrays [["value", 1.0]], or an array of hashes of value and score [{value: "value", score: 1.0}].
276
+ # @param collection_ttl [Integer] time-to-live, in seconds.
277
+ # @raise [ArgumentError] if the ttl is invalid
278
+ # @return [Momento::SortedSetPutElementsResponse]
279
+ # @raise [TypeError] when the cache_name, or sorted_set_name is not a String, or if elements is not
280
+ # an Array or Hash
281
+ def sorted_set_put_elements(cache_name, sorted_set_name, elements, collection_ttl = CollectionTtl.from_cache_ttl)
282
+ collection_ttl = collection_ttl.with_ttl_if_absent(default_ttl.seconds)
283
+ builder = SortedSetPutElementsResponseBuilder.new(
284
+ context: { cache_name: cache_name, set_name: sorted_set_name, elements: elements,
285
+ collection_ttl: collection_ttl }
286
+ )
287
+
288
+ builder.from_block do
289
+ req = MomentoProtos::CacheClient::PB__SortedSetPutRequest.new(
290
+ set_name: to_bytes(sorted_set_name),
291
+ elements: to_sorted_set_elements(elements),
292
+ ttl_milliseconds: collection_ttl.ttl_milliseconds,
293
+ refresh_ttl: collection_ttl.refresh_ttl
294
+ )
295
+
296
+ # noinspection RubyResolve
297
+ cache_stub.sorted_set_put(req, metadata: grpc_metadata(cache_name))
298
+ end
299
+ end
300
+
301
+ # rubocop:disable Metrics/ParameterLists
302
+
303
+ # Fetch the elements a sorted set by score.
304
+ #
305
+ # @example
306
+ # response = client.sorted_set_fetch_by_score("my_cache", "sorted_set", min_score: 0.0, max_score: 1.0)
307
+ # raise response.error if response.error?
308
+ #
309
+ # @see Momento::SortedSetFetchResponse
310
+ # @param cache_name [String]
311
+ # @param sorted_set_name [String]
312
+ # @param min_score [Float] The minimum score (inclusive) of the elements to fetch. Defaults to negative infinity.
313
+ # @param max_score [Float] The maximum score (inclusive) of the elements to fetch. Defaults to positive infinity.
314
+ # @param sort_order [SortOrder] The order to fetch the elements in. Defaults to ascending.
315
+ # @param offset [Integer] The number of elements to skip before returning the first element. Defaults to 0.
316
+ # @param count [Integer] The maximum number of elements to return. Defaults to all elements.
317
+ # @return [Momento::SortedSetFetchResponse]
318
+ # @raise [TypeError] when the cache_name, or sorted_set_name is not a String.
319
+ def sorted_set_fetch_by_score(cache_name, sorted_set_name, min_score: nil, max_score: nil,
320
+ sort_order: SortOrder::ASCENDING, offset: 0, count: -1)
321
+ builder = SortedSetFetchResponseBuilder.new(
322
+ context: { cache_name: cache_name, set_name: sorted_set_name, min_score: min_score, max_score: max_score,
323
+ sort_order: sort_order, offset: offset, count: count }
324
+ )
325
+
326
+ builder.from_block do
327
+ by_score = build_sorted_set_by_score(min_score, max_score, offset, count)
328
+
329
+ req = MomentoProtos::CacheClient::PB__SortedSetFetchRequest.new(
330
+ set_name: to_bytes(sorted_set_name),
331
+ order: to_grpc_order(sort_order),
332
+ with_scores: true,
333
+ by_score: by_score
334
+ )
335
+
336
+ # noinspection RubyResolve
337
+ cache_stub.sorted_set_fetch(req, metadata: grpc_metadata(cache_name))
338
+ end
339
+ end
340
+ # rubocop:enable Metrics/ParameterLists
341
+
342
+ private
343
+
344
+ def cache_stub
345
+ @cache_stubs ||= (1..@num_cache_stubs).map {
346
+ CACHE_CLIENT_STUB_CLASS.new(@cache_endpoint, combined_credentials,
347
+ timeout: @configuration.transport_strategy.grpc_configuration.deadline,
348
+ channel_args: { 'grpc.use_local_subchannel_pool' => 1 }
349
+ )
350
+ }
351
+ @next_cache_stub_index = (@next_cache_stub_index + 1) % @num_cache_stubs
352
+ @cache_stubs[@next_cache_stub_index]
353
+ end
354
+
355
+ def control_stub
356
+ @control_stub ||= CONTROL_CLIENT_STUB_CLASS.new(@control_endpoint, combined_credentials)
357
+ end
358
+
359
+ def combined_credentials
360
+ @combined_credentials ||= make_combined_credentials
361
+ end
362
+
363
+ def make_combined_credentials
364
+ # :nocov:
365
+ auth_proc = proc do
366
+ { authorization: @api_key }
367
+ end
368
+ # :nocov:
369
+
370
+ call_creds = GRPC::Core::CallCredentials.new(auth_proc)
371
+
372
+ GRPC::Core::ChannelCredentials.new.compose(call_creds)
373
+ end
374
+
375
+ def to_grpc_order(sort_order)
376
+ case sort_order
377
+ when SortOrder::ASCENDING
378
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::Order::ASCENDING
379
+ when SortOrder::DESCENDING
380
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::Order::DESCENDING
381
+ else
382
+ raise TypeError, "Invalid sort order: #{sort_order}"
383
+ end
384
+ end
385
+
386
+ # Momento accepts sorted sets as an array of hashes. This will transform an array of arrays [["value", 1.0]],
387
+ # an array of hashes [{value: "value", score: 1.0}], or a hash of values to scores to the correct format.
388
+ # @param elements [Hash, Array] A hash of string values to scores or an array of tuples (value, score)
389
+ # # @return [Array<Hash>] An array of sorted set elements, where each element is a hash of :value and :score
390
+ def to_sorted_set_elements(elements)
391
+ case elements
392
+ when Hash
393
+ elements.map { |value, score| { value: to_bytes(value), score: score.to_f } }
394
+ when Array
395
+ if elements.first.is_a?(Hash)
396
+ elements.map { |element| { value: to_bytes(element[:value]), score: element[:score].to_f } }
397
+ else
398
+ elements.map { |value, score| { value: to_bytes(value), score: score.to_f } }
399
+ end
400
+ else
401
+ raise ArgumentError, "Sorted set elements must be a Hash or an Array of tuples"
402
+ end
403
+ end
404
+
405
+ def build_sorted_set_by_score(min_score, max_score, offset, count)
406
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::PB__ByScore.new(
407
+ min_score: min_score ? build_score(min_score) : nil,
408
+ unbounded_min: min_score ? nil : MomentoProtos::Common::PB__Unbounded.new,
409
+ max_score: max_score ? build_score(max_score) : nil,
410
+ unbounded_max: max_score ? nil : MomentoProtos::Common::PB__Unbounded.new,
411
+ offset: offset,
412
+ count: count
413
+ )
414
+ end
415
+
416
+ def build_score(score)
417
+ MomentoProtos::CacheClient::PB__SortedSetFetchRequest::PB__ByScore::PB__Score.new(
418
+ score: score,
419
+ exclusive: false
420
+ )
421
+ end
422
+
423
+ # Ruby uses String for bytes. GRPC wants a String encoded as ASCII.
424
+ # GRPC will re-encode a String, but treats it as characters; GRPC will
425
+ # raise if you pass a String with non-ASCII characters.
426
+ # So we do the re-encoding ourselves in a way that treats the String as
427
+ # bytes and will not raise. The data is not changed.
428
+ #
429
+ # If the input String is ASCII, we treat it as binary data. Otherwise,
430
+ # we ensure it is encoded as UTF-8 to stop the SDK from being able to
431
+ # write non-UTF-8 strings to the server.
432
+ #
433
+ # A duplicate String is returned, but since Ruby is copy-on-write it
434
+ # does not copy the data.
435
+ #
436
+ # @param string [String] the string to make safe for GRPC bytes
437
+ # @return [String] a duplicate safe to use as GRPC bytes
438
+ # @raise [TypeError] when the string is not a String
439
+ def to_bytes(string)
440
+ raise TypeError, "expected a String, got a #{string.class}" unless string.is_a?(String)
441
+
442
+ if string.encoding == Encoding::ASCII_8BIT
443
+ string.dup
444
+ else
445
+ utf8_encoded = string.encode('UTF-8')
446
+ utf8_encoded.force_encoding(Encoding::ASCII_8BIT)
447
+ end
448
+ end
449
+
450
+ def grpc_metadata(cache_name)
451
+ if @is_first_request
452
+ @is_first_request = false
453
+ {
454
+ cache: validate_cache_name(cache_name),
455
+ Agent: "ruby:cache:#{VERSION}",
456
+ 'Runtime-Version': "ruby:#{RUBY_VERSION}"
457
+ }
458
+ else
459
+ { cache: validate_cache_name(cache_name) }
460
+ end
461
+ end
462
+
463
+ # Return a UTF-8 version of the cache name.
464
+ #
465
+ # @param name [String] the cache name to validate
466
+ # @raise [TypeError] when the name is not a String
467
+ # @raise [Momento::CacheNameError] when the name is not UTF-8 compatible
468
+ def validate_cache_name(name)
469
+ raise TypeError, "Cache name must be a String, got a #{name.class}" unless name.is_a?(String)
470
+
471
+ encoded_name = name.encode('UTF-8')
472
+ raise Momento::CacheNameError, "Cache name must be UTF-8 compatible" unless name.valid_encoding?
473
+
474
+ encoded_name
475
+ end
476
+ end
477
+ # rubocop:enable Metrics/ClassLength
478
+ end