module Moneta # @api private module OptionSupport # Return Moneta store with default options or additional proxies # # @param [Hash] options Options to merge # @return [Moneta store] # # @api public def with(options = nil, &block) adapter = self if block builder = Builder.new(&block) builder.adapter(adapter) adapter = builder.build.last end options ? OptionMerger.new(adapter, options) : adapter end # Return Moneta store with default option raw: true # # @return [OptionMerger] # @api public def raw @raw_store ||= begin store = with(raw: true, only: [:load, :store, :create, :delete]) store.instance_variable_set(:@raw_store, store) store end end # Return Moneta store with default prefix option # # @param [String] prefix Key prefix # @return [OptionMerger] # @api public def prefix(prefix) with(prefix: prefix, except: :clear) end # Return Moneta store with default expiration time # # @param [Integer] expires Default expiration time # @return [OptionMerger] # @api public def expires(expires) with(expires: expires, only: [:store, :create, :increment]) end end # Simple interface to key/value stores with Hash-like interface. # @api public module Defaults include OptionSupport # @api private module ClassMethods # Returns features list # # @return [Array] list of features def features @features ||= superclass.respond_to?(:features) ? superclass.features : [].freeze end # Declares that this adapter supports the given feature. # # @example # class MyAdapter # include Moneta::Defaults # supports :create # def create(key, value, options = {}) # # implement create! # end # end def supports(*features) @features = (self.features + features).uniq.freeze end end def self.included(base) base.extend(ClassMethods) end # Exists the value with key # # @param [Object] key # @param [Hash] options # @option options [Integer] :expires Update expiration time (See {Expires}) # @option options [String] :prefix Prefix key (See {Transformer}) # @option options Other options as defined by the adapters or middleware # @return [Boolean] # @api public def key?(key, options = {}) load(key, options) != nil end # Atomically increment integer value with key # # This method also accepts negative amounts. # # @note Not every Moneta store implements this method, # a NotImplementedError is raised if it is not supported. # @param [Object] key # @param [Integer] amount # @param [Hash] options # @option options [String] :prefix Prefix key (See {Transformer}) # @option options Other options as defined by the adapters or middleware # @return [Object] value from store # @api public def increment(key, amount = 1, options = {}) raise NotImplementedError, 'increment is not supported' end # Atomically decrement integer value with key # # This is just syntactic sugar for calling #increment with a negative value. # # This method also accepts negative amounts. # # @param [Object] key # @param [Integer] amount # @param [Hash] options # @option options [String] :prefix Prefix key (See {Transformer}) # @option options Other options as defined by the adapters or middleware # @return [Object] value from store # @api public def decrement(key, amount = 1, options = {}) increment(key, -amount, options) end # Explicitly close the store # @return nil # @api public def close end # Fetch a value with a key # # @overload fetch(key, options = {}, &block) # retrieve a key. if the key is not available, execute the # block and return its return value. # @param [Object] key # @param [Hash] options # @option options [Integer] :expires Update expiration time (See {Expires}) # @option options [Boolean] :raw Raw access without value transformation (See {Transformer}) # @option options [String] :prefix Prefix key (See {Transformer}) # @return [Object] value from store # # @overload fetch(key, default, options = {}) # retrieve a key. if the key is not available, return the default value. # @param [Object] key # @param [Object] default Default value # @param [Hash] options # @option options [Integer] :expires Update expiration time (See {Expires}) # @option options [Boolean] :raw Raw access without value transformation (See {Transformer}) # @option options [String] :prefix Prefix key (See {Transformer}) # @return [Object] value from store # # @api public def fetch(key, default = nil, options = nil) if block_given? raise ArgumentError, 'Only one argument accepted if block is given' if options result = load(key, default || {}) result == nil ? yield(key) : result else result = load(key, options || {}) result == nil ? default : result end end # Fetch value with key. Return nil if the key doesn't exist # # @param [Object] key # @return [Object] value # @api public def [](key) load(key) end # Store value with key # # @param [Object] key # @param [Object] value # @return value # @api public def []=(key, value) store(key, value) end # Atomically sets a key to value if it's not set. # # @note Not every Moneta store implements this method, # a NotImplementedError is raised if it is not supported. # @param [Object] key # @param [Object] value # @param [Hash] options # @option options [Integer] :expires Update expiration time (See {Expires}) # @option options [Boolean] :raw Raw access without value transformation (See {Transformer}) # @option options [String] :prefix Prefix key (See {Transformer}) # @return [Boolean] key was set # @api public def create(key, value, options = {}) raise NotImplementedError, 'create is not supported' end # Returns features list # # @return [Array] list of features def features self.class.features end # Return true if adapter supports the given feature. # # @return [Boolean] def supports?(feature) features.include?(feature) end end # @api private module IncrementSupport # (see Defaults#increment) def increment(key, amount = 1, options = {}) value = Utils.to_int(load(key, options)) + amount store(key, value.to_s, options) value end def self.included(base) base.supports(:increment) if base.respond_to?(:supports) end end # Implements simple create using key? and store. # # This is sufficient for non-shared stores or if atomicity is not required. # @api private module CreateSupport # (see Defaults#create) def create(key, value, options = {}) if key? key false else store(key, value, options) true end end def self.included(base) base.supports(:create) if base.respond_to?(:supports) end end # @api private module HashAdapter attr_reader :backend # (see Proxy#key?) def key?(key, options = {}) @backend.has_key?(key) end # (see Proxy#load) def load(key, options = {}) @backend[key] end # (see Proxy#store) def store(key, value, options = {}) @backend[key] = value end # (see Proxy#delete) def delete(key, options = {}) @backend.delete(key) end # (see Proxy#clear) def clear(options = {}) @backend.clear self end end # This mixin handles the calculation of expiration times. # # module ExpiresSupport attr_accessor :default_expires protected # Calculates the time when something will expire. # # This method considers false and 0 as "no-expire" and every positive # number as a time to live in seconds. # # @param [Hash] options Options hash # @option options [0,false,nil,Numeric] :expires expires value given by user # @param [0,false,nil,Numeric] default default expiration time # # @return [false] if it should not expire # @return [Time] the time when something should expire # @return [nil] if it is not known def expires_at(options, default = @default_expires) value = expires_value(options, default) Numeric === value ? Time.now + value : value end # Calculates the number of seconds something should last. # # This method considers false and 0 as "no-expire" and every positive # number as a time to live in seconds. # # @param [Hash] options Options hash # @option options [0,false,nil,Numeric] :expires expires value given by user # @param [0,false,nil,Numeric] default default expiration time # # @return [false] if it should not expire # @return [Numeric] seconds until expiration # @return [nil] if it is not known def expires_value(options, default = @default_expires) case value = options[:expires] when 0, false false when nil default ? default.to_i : nil when Numeric value = value.to_i raise ArgumentError, ":expires must be a positive value, got #{value}" if value < 0 value else raise ArgumentError, ":expires must be Numeric or false, got #{value.inspect}" end end def self.included(base) base.supports(:expires) if base.respond_to?(:supports) end end end