lib/momento/simple_cache_client.rb in momento-0.1.0 vs lib/momento/simple_cache_client.rb in momento-0.2.0

- old
+ new

@@ -1,145 +1,258 @@ require 'jwt' -require 'momento/cacheclient_services_pb' -require 'momento/controlclient_services_pb' -require 'momento/response' +require_relative 'cacheclient_services_pb' +require_relative 'controlclient_services_pb' +require_relative 'response' +require_relative 'ttl' +require_relative 'exceptions' module Momento + # rubocop:disable Metrics/ClassLength + # A simple client for Momento. # + # SimpleCacheClient does not use exceptions to report errors. + # Instead it returns an error response. Please see {file:README.md#label-Error+Handling}. + # # @example + # token = ...your Momento JWT... # client = Momento::SimpleCacheClient.new( - # auth_token: jwt, - # default_ttl: 10_000 + # auth_token: token, + # # cached items will be deleted after 100 seconds + # default_ttl: 100 # ) # + # response = client.create_cache("my_cache") + # if response.success? + # puts "my_cache was created" + # elsif response.already_exists? + # puts "my_cache already exists" + # elsif response.error? + # raise response.error + # end + # + # # set will only return success or error, + # # we only need to check for error + # response = client.set("my_cache", "key", "value") + # raise response.error if response.error? + # # response = client.get("my_cache", "key") # if response.hit? - # puts "We got #{response}" + # puts "We got #{response.value_string}" # elsif response.miss? # puts "It's not in the cache" # elsif response.error? - # puts "The front fell off." + # raise response.error # end + # + # @see Momento::Response class SimpleCacheClient + # This gem's version. VERSION = Momento::VERSION CACHE_CLIENT_STUB_CLASS = CacheClient::Scs::Stub CONTROL_CLIENT_STUB_CLASS = ControlClient::ScsControl::Stub + private_constant :CACHE_CLIENT_STUB_CLASS, :CONTROL_CLIENT_STUB_CLASS - # The default time to live, in milliseconds. + # @return [Numeric] how long items should remain in the cache, in seconds. attr_accessor :default_ttl # @param auth_token [String] the JWT for your Momento account - # @param default_ttl [Integer] + # @param default_ttl [Numeric] time-to-live, in seconds + # @raise [ArgumentError] if the default_ttl or auth_token is invalid def initialize(auth_token:, default_ttl:) @auth_token = auth_token - @default_ttl = default_ttl + @default_ttl = Momento::Ttl.to_ttl(default_ttl) load_endpoints_from_token end # Get a value in a cache. # - # Momento only stores bytes; the returned value will be encoded as ASCII-8BIT. + # The value can be retrieved as either bytes or a string. + # @example + # response = client.get("my_cache", "key") + # if response.hit? + # puts "We got #{response.value_string}" + # elsif response.miss? + # puts "It's not in the cache" + # elsif response.error? + # raise response.error + # end # + # @see Momento::GetResponse # @param cache_name [String] # @param key [String] must only contain ASCII characters # @return [Momento::GetResponse] + # @raise [TypeError] when the cache_name or key is not a String def get(cache_name, key) - return GetResponse.from_block do + builder = GetResponseBuilder.new( + context: { cache_name: cache_name, key: key } + ) + + return builder.from_block do + validate_cache_name(cache_name) + cache_stub.get( CacheClient::GetRequest.new(cache_key: to_bytes(key)), metadata: { cache: cache_name } ) end end # Set a value in a cache. # # If ttl is not set, it will use the default_ttl. + # @example + # response = client.set("my_cache", "key", "value") + # raise response.error if response.error? # + # @see Momento::SetResponse # @param cache_name [String] # @param key [String] must only contain ASCII characters # @param value [String] the value to cache - # @param ttl [Integer] time to live, in milliseconds. + # @param ttl [Numeric] time-to-live, in seconds. + # @raise [ArgumentError] if the ttl is invalid # @return [Momento::SetResponse] + # @raise [TypeError] when the cache_name, key, or value is not a String def set(cache_name, key, value, ttl: default_ttl) - return SetResponse.from_block do + ttl = Momento::Ttl.to_ttl(ttl) + + builder = SetResponseBuilder.new( + context: { cache_name: cache_name, key: key, value: value, ttl: ttl } + ) + + return builder.from_block do + validate_cache_name(cache_name) + req = CacheClient::SetRequest.new( cache_key: to_bytes(key), cache_body: to_bytes(value), - ttl_milliseconds: ttl + ttl_milliseconds: ttl.milliseconds ) cache_stub.set(req, metadata: { cache: cache_name }) end end # Delete a key in a cache. # + # If the key does not exist, delete will still succeed. + # @example + # response = client.delete("my_cache", "key") + # raise response.error if response.error? + # + # @see Momento::DeleteResponse # @param cache_name [String] # @param key [String] must only contain ASCII characters # @return [Momento::DeleteResponse] + # @raise [TypeError] when the cache_name or key is not a String def delete(cache_name, key) - return DeleteResponse.from_block do + builder = DeleteResponseBuilder.new( + context: { cache_name: cache_name, key: key } + ) + + return builder.from_block do + validate_cache_name(cache_name) + cache_stub.delete( CacheClient::DeleteRequest.new(cache_key: to_bytes(key)), metadata: { cache: cache_name } ) end end # Create a new Momento cache. + # @example + # response = client.create_cache("my_cache") + # if response.success? + # puts "my_cache was created" + # elsif response.already_exists? + # puts "my_cache already exists" + # elsif response.error? + # raise response.error + # end # - # @param name [String] the name of the cache to create. + # @see Momento::CreateCacheResponse + # @param cache_name [String] the name of the cache to create. # @return [Momento::CreateCacheResponse] the response from Momento. - def create_cache(name) - return CreateCacheResponse.from_block do + # @raise [TypeError] when the cache_name is not a String + def create_cache(cache_name) + builder = CreateCacheResponseBuilder.new( + context: { cache_name: cache_name } + ) + + return builder.from_block do + validate_cache_name(cache_name) + control_stub.create_cache( - ControlClient::CreateCacheRequest.new(cache_name: name) + ControlClient::CreateCacheRequest.new(cache_name: cache_name) ) end end # Delete an existing Momento cache. # - # @param name [String] the name of the cache to delete. + # @example + # response = client.delete_cache("my_cache") + # raise response.error if response.error? + # + # @see Momento::DeleteCacheResponse + # @param cache_name [String] the name of the cache to delete. # @return [Momento::DeleteCacheResponse] the response from Momento. - def delete_cache(name) - return DeleteCacheResponse.from_block do + # @raise [TypeError] when the cache_name is not a String + def delete_cache(cache_name) + builder = DeleteCacheResponseBuilder.new( + context: { cache_name: cache_name } + ) + + return builder.from_block do + validate_cache_name(cache_name) + control_stub.delete_cache( - ControlClient::DeleteCacheRequest.new(cache_name: name) + ControlClient::DeleteCacheRequest.new(cache_name: cache_name) ) end end # List a page of your caches. # + # This is a low-level method. You probably want to use {#caches} instead. + # # The next_token indicates which page to fetch. # If nil or "" it will fetch the first page. Default is to fetch the first page. # - # @params next_token [String, nil] the token of the page to request + # @see #caches + # @see Momento::ListCachesResponse + # @note Consider using `caches` instead. + # @param next_token [String, nil] the token of the page to request # @return [Momento::ListCachesResponse] def list_caches(next_token: "") - return ListCachesResponse.from_block do + builder = ListCachesResponseBuilder.new( + context: { next_token: next_token } + ) + return builder.from_block do control_stub.list_caches( ControlClient::ListCachesRequest.new(next_token: next_token) ) end end - # Lists the names of all your caches. + # Lists the names of all your caches, as a lazy Enumerator. + # @example + # cache_names = client.caches + # cache_names.each { |name| puts name } # + # @note Unlike other methods, this will raise if there is a problem + # with the client or service. # @return [Enumerator::Lazy<String>] the cache names - # @raise [GRPC::BadStatus] - # rubocop:disable Metrics/MethodLength + # @raise [Momento::Error] when there is an error listing caches. def caches Enumerator.new do |yielder| next_token = "" loop do response = list_caches(next_token: next_token) - raise response.grpc_exception if response.is_a? Momento::Response::Error + raise response.error if response.is_a? Momento::Response::Error response.cache_names.each do |name| yielder << name end @@ -147,11 +260,10 @@ next_token = response.next_token end end.lazy end - # rubocop:enable Metrics/MethodLength private def cache_stub @cache_stub ||= CACHE_CLIENT_STUB_CLASS.new(@cache_endpoint, combined_credentials) @@ -168,10 +280,12 @@ def load_endpoints_from_token claim = JWT.decode(@auth_token, nil, false).first @control_endpoint = claim["cp"] @cache_endpoint = claim["c"] + rescue JWT::DecodeError + raise ArgumentError, "Invalid Momento auth token." end def make_combined_credentials # :nocov: auth_proc = proc do @@ -193,12 +307,30 @@ # A duplicate String is returned, but since Ruby is copy-on-write it # does not copy the data. # # @param string [String] the string to make safe for GRPC bytes # @return [String] a duplicate safe to use as GRPC bytes + # @raise [TypeError] when the string is not a String def to_bytes(string) + raise TypeError, "expected a String, got a #{string.class}" unless string.is_a?(String) + # dup in case the value is frozen and to avoid changing the value's encoding # for the caller. return string.dup.force_encoding(Encoding::ASCII_8BIT) end + + # This is not a complete validation of the cache name, just + # issues that might cause an exception in the client. Let the server + # handle the rest of the validation. + # + # @param name [String] the cache name to validate + # @raise [TypeError] when the name is not a String + # @raise [Momento::CacheNameError] when the name is not ASCII + def validate_cache_name(name) + raise TypeError, "Cache name must be a String, got a #{name.class}" unless name.is_a?(String) + raise Momento::CacheNameError, "Cache name must be ASCII, got '#{name}'" if name.match?(/[^[:ascii:]]/) + + return + end end + # rubocop:enable Metrics/ClassLength end