momento 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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