diff --git a/lib/moneta.rb b/lib/moneta.rb index 60fa7c8f..3a1bbd6a 100644 --- a/lib/moneta.rb +++ b/lib/moneta.rb @@ -19,6 +19,7 @@ module Moneta autoload :IncrementSupport, 'moneta/increment_support' autoload :Lock, 'moneta/lock' autoload :Logger, 'moneta/logger' + autoload :Metadata, 'moneta/metadata' autoload :Mutex, 'moneta/synchronize' autoload :NilValues, 'moneta/nil_values' autoload :OptionMerger, 'moneta/optionmerger' diff --git a/lib/moneta/expires.rb b/lib/moneta/expires.rb index f8dba846..68374b76 100644 --- a/lib/moneta/expires.rb +++ b/lib/moneta/expires.rb @@ -1,3 +1,6 @@ +require 'forwardable' +require 'set' + module Moneta # Adds expiration support to the underlying store # @@ -6,183 +9,194 @@ module Moneta # # @api public class Expires < Proxy + extend Forwardable include ExpiresSupport + def_delegator :adapter, :metadata_names + # @param [Moneta store] adapter The underlying store # @param [Hash] options + # @option options [] :metadata A list of metadata field names to + # use # @option options [String] :expires Default expiration time def initialize(adapter, options = {}) raise 'Store already supports feature :expires' if adapter.supports?(:expires) + if !adapter.supports?(:metadata) || !adapter.metadata_names.include?(:expires) + metadata = options.delete(:metadata) || [] + metadata.push(:expires) unless metadata.include?(:expires) + adapter = Metadata.new(adapter, options.merge(metadata: metadata)) + end + self.default_expires = options[:expires] super end # (see Proxy#key?) def key?(key, options = {}) - # Transformer might raise exception - load_entry(key, options) != nil - rescue - super(key, Utils.without(options, :expires)) + return super if options.include?(:raw) + begin + nil != load_or_expire(key: key, return_metadata: true, options: Utils.without(options, :return_metadata)) + rescue + # Fallback for if the key is present but can't be loaded + super(key, Utils.without(options, :expires)) + end end - # (see Proxy#load) + # (see Metadata#load) def load(key, options = {}) return super if options.include?(:raw) - value, = load_entry(key, options) - value + return_metadata = options[:return_metadata] + load_or_expire(key: key, return_metadata: return_metadata, options: options) end - # (see Proxy#store) + # (see Metadata#store) def store(key, value, options = {}) return super if options.include?(:raw) expires = expires_at(options) - super(key, new_entry(value, expires), Utils.without(options, :expires)) - value + options_with_metadata = update_options_with_metadata(expires: expires, options: options) + super(key, value, options_with_metadata) end - # (see Proxy#delete) + # (see Metadata#delete) def delete(key, options = {}) return super if options.include?(:raw) - value, expires = super - value if !expires || Time.now <= Time.at(expires) + return_metadata = options[:return_metadata] + if struct = load_or_expire(key: key, return_metadata: true, options: options, allow_expiry_update: false) + super(key, options) + return_metadata ? struct : struct.value + end end - # (see Proxy#store) + # (see Metadata#store) def create(key, value, options = {}) return super if options.include?(:raw) expires = expires_at(options) - @adapter.create(key, new_entry(value, expires), Utils.without(options, :expires)) + options_with_metadata = update_options_with_metadata(expires: expires, options: options) + @adapter.create(key, value, options_with_metadata) end - # (see Defaults#values_at) - def values_at(*keys, **options) + # (see Metadata#values_at) + def values_at(*keys, return_metadata: false, **options) return super if options.include?(:raw) - new_expires = expires_at(options, nil) - options = Utils.without(options, :expires) - with_updates(options) do |updates| - keys.zip(@adapter.values_at(*keys, **options)).map do |key, entry| - entry = invalidate_entry(key, entry, new_expires) do |new_entry| - updates[key] = new_entry - end - next if entry == nil - value, = entry - value + expires = expires_at(options, nil) + options = Utils.without(options, :expires).merge(return_metadata: true) + structs = @adapter.values_at(*keys, **options) + + keys.zip(structs).map do |key, struct| + next if struct == nil || delete_if_expired(key: key, struct: struct) + if expires != nil + options_with_metadata = update_options_with_metadata(expires: expires, options: options, metadata: struct.to_h) + struct = @adapter.store(key, struct.value, options_with_metadata) end + return_metadata ? struct : struct.value end end - # (see Defaults#fetch_values) - def fetch_values(*keys, **options) + # (see Metadata#fetch_values) + def fetch_values(*keys, return_metadata: false, **options) return super if options.include?(:raw) - new_expires = expires_at(options, nil) - options = Utils.without(options, :expires) - substituted = {} + expires = expires_at(options, nil) + options = Utils.without(options, :expires).merge(return_metadata: true) + + substituted = Set.new block = if block_given? lambda do |key| - substituted[key] = true + substituted << key yield key end end - with_updates(options) do |updates| - keys.zip(@adapter.fetch_values(*keys, **options, &block)).map do |key, entry| - next entry if substituted[key] - entry = invalidate_entry(key, entry, new_expires) do |new_entry| - updates[key] = new_entry + structs = @adapter.fetch_values(*keys, **options, &block) + keys.zip(structs).map do |key, struct| + unless substituted.include? key + next if struct == nil + if delete_if_expired(key: key, struct: struct) + if block_given? + struct.value = yield key + else + next + end + elsif expires != nil + options_with_metadata = update_options_with_metadata(expires: expires, options: options, metadata: struct.to_h) + struct = @adapter.store(key, struct.value, options_with_metadata) end - if entry == nil - value = if block_given? - yield key - end - else - value, = entry - end - value end + return_metadata ? struct : struct.value end end - # (see Defaults#slice) + # (see Metadata#slice) def slice(*keys, **options) return super if options.include?(:raw) - new_expires = expires_at(options, nil) - options = Utils.without(options, :expires) - - with_updates(options) do |updates| - @adapter.slice(*keys, **options).map do |key, entry| - entry = invalidate_entry(key, entry, new_expires) do |new_entry| - updates[key] = new_entry - end - next if entry == nil - value, = entry - [key, value] - end.reject(&:nil?) + return_metadata = options[:return_metadata] + expires = expires_at(options, nil) + options = Utils.without(options, :expires).merge(return_metadata: true) + + @adapter.slice(*keys, **options).each_with_object([]) do |(key, struct), slice| + next if delete_if_expired(key: key, struct: struct) + if expires != nil + options_with_metadata = update_options_with_metadata(expires: expires, options: options, metadata: struct.to_h) + struct = @adapter.store(key, struct.value, options_with_metadata) + end + slice.push [key, return_metadata ? struct : struct.value] end end - # (see Defaults#merge!) + # (see Metadata#merge!) def merge!(pairs, options = {}) + yield_metadata = options[:yield_metadata] expires = expires_at(options) - options = Utils.without(options, :expires) + options = Utils.without(options, :expires).merge(yield_metadata: true) + options_with_metadata = update_options_with_metadata(expires: expires, options: options) block = if block_given? - lambda do |key, old_entry, entry| - old_entry = invalidate_entry(key, old_entry) - if old_entry == nil - entry # behave as if no replace is happening + lambda do |key, old_struct, struct| + next struct if delete_if_expired(key: key, struct: old_struct) + + if yield_metadata + yield key, old_struct, struct else - old_value, = old_entry - new_value, = entry - new_entry(yield(key, old_value, new_value), expires) + struct.value = yield key, old_struct.value, struct.value + struct end end end - entry_pairs = pairs.map do |key, value| - [key, new_entry(value, expires)] - end - @adapter.merge!(entry_pairs, options, &block) + @adapter.merge!(pairs, options_with_metadata, &block) self end private - def load_entry(key, options) - new_expires = expires_at(options, nil) - options = Utils.without(options, :expires) - entry = @adapter.load(key, options) - invalidate_entry(key, entry, new_expires) do |new_entry| - @adapter.store(key, new_entry, options) - end - end - - def invalidate_entry(key, entry, new_expires = nil) - if entry != nil - value, expires = entry - if expires && Time.now > Time.at(expires) - delete(key) - entry = nil - elsif new_expires != nil - yield new_entry(value, new_expires) if block_given? + def load_or_expire(key:, options:, return_metadata: false, allow_expiry_update: true) + options = options.merge(return_metadata: true) + struct = @adapter.load(key, options) + return if struct == nil + struct = + if delete_if_expired(key: key, struct: struct) + nil + elsif allow_expiry_update && (expires = expires_at(options, nil)) != nil + options_with_metadata = update_options_with_metadata(expires: expires, options: options, metadata: struct.to_h) + @adapter.store(key, struct.value, options_with_metadata) + else + struct end - end - entry + + struct && (return_metadata ? struct : struct.value) end - def new_entry(value, expires) - if expires - [value, expires.to_r] - elsif Array === value || value == nil - [value] + def delete_if_expired(key:, struct:) + if struct.expires && Time.now > Time.at(struct.expires) + @adapter.delete key + true else - value + false end end - def with_updates(options) - updates = {} - yield(updates).tap do - @adapter.merge!(updates, options) unless updates.empty? - end + def update_options_with_metadata(expires:, options:, metadata: nil) + metadata ||= options[:metadata].to_h + Utils.without(options, :expires, :metadata).merge \ + metadata: metadata.merge(expires: expires ? expires.to_r : nil) end end end diff --git a/lib/moneta/metadata.rb b/lib/moneta/metadata.rb new file mode 100644 index 00000000..745aa816 --- /dev/null +++ b/lib/moneta/metadata.rb @@ -0,0 +1,161 @@ +module Moneta + # Adds metadata support + # + # @api public + class Metadata < Proxy + attr_reader :metadata_names + + supports :metadata + + # @param [Moneta store] adapter The underlying store + # @param [Hash] options + # @option options [] :metadata A list of metadata field names to + # use + def initialize(adapter, options = {}) + raise 'Store already supports feature :metadata' if adapter.supports?(:metadata) + @metadata_names = options.delete(:metadata).to_a.freeze + super + raise ":value is reserved" if metadata_names.include?(:value) + @struct = Struct.new(:value, *metadata_names) + end + + # (see Proxy#create) + # @option options [{Symbol => String}] :metadata A hash of metadata values to store + def create(key, value, options = {}) + return super if options.include?(:raw) + metadata_hash = options[:metadata].to_h + values = value_with_metadata_hash(value, metadata_hash) + super(key, values, options) + end + + # (see Proxy#delete) + # @option options [Boolean] :return_metadata If true, return a struct including all metadata + def delete(key, options = {}) + return super if options.include?(:raw) + return_metadata = options[:return_metadata] + values = super(key, options) + return_metadata ? values_to_struct(values) : values_to_value(values) + end + + # (see Proxy#fetch_values) + # @param return_metadata [Boolean] :return_metadata If true, each fetched value + # is returned as a struct including all metadata + def fetch_values(*keys, return_metadata: false, raw: false, **options) + return super if raw + block = if block_given? + lambda { |key| [yield(key)] } + end + + @adapter + .fetch_values(*keys, **options, &block) + .map(&method(return_metadata ? :values_to_struct : :values_to_value)) + end + + # (see Proxy#load) + # @option options [Boolean] :return_metadata If true, return a struct + # including all metadata + def load(key, options = {}) + return super if options.include?(:raw) + return_metadata = options[:return_metadata] + values = super(key, options) + return_metadata ? values_to_struct(values) : values_to_value(values) + end + + # (see Proxy#merge!) + # @option options [Boolean] :yield_metadata If true, and a block is + # provided, the block will receive structs including all metadata for + # each existing value. This can be used to merge any existing metadata. + # @option options [{Symbol => String}] :metadata The metadata that should + # be associated with all stored values. + def merge!(pairs, options = {}) + return super if options.include?(:raw) + + block = if block_given? + return_metadata = options[:yield_metadata] + lambda do |key, *old_and_new| + if return_metadata + struct = yield(key, *old_and_new.map(&method(:values_to_struct))) + struct.to_a + else + values = old_and_new.last + values[0] = yield(key, *old_and_new.map(&method(:values_to_value))) + values + end + end + end + + metadata_values = metadata_values_from_hash(options[:metadata].to_h) + pairs_with_metadata = pairs.map do |key, value| + [key, value_with_metadata_values(value, metadata_values)] + end + + @adapter.merge!(pairs_with_metadata, options, &block) + self + end + + # (see Proxy#slice) + # @param [Boolean] return_metadata if true, each value returned will be a + # struct including all metadata + def slice(*keys, return_metadata: false, raw: false, **options) + return super if raw + values_slice = @adapter.slice(*keys, **options) + if return_metadata + values_slice.map { |key, values| [key, values_to_struct(values)] } + else + values_slice.map { |key, values| [key, values_to_value(values)] } + end + end + + # (see Proxy#store) + # @option options [{Symbol => String}] :metadata A hash of metadata to + # store + # @option options [Boolean] :return_metadata If true, this method will return + # a struct including the metadata that was stored + def store(key, value, options = {}) + return super if options.include?(:raw) + metadata_hash = options[:metadata].to_h + return_metadata = options[:return_metadata] + values = value_with_metadata_hash(value, metadata_hash) + super(key, values, options) + return_metadata ? values_to_struct(values) : value + end + + # (see Proxy#values_at) + # @param [Boolean] return_metadata If true, each value loaded will be + # returned as a struct including any metadata. + def values_at(*keys, return_metadata: false, raw: false, **options) + return super if raw + @adapter + .values_at(*keys, **options) + .map(&method(return_metadata ? :values_to_struct : :values_to_value)) + end + + private + + def metadata_values_from_hash(metadata_hash) + metadata_hash.values_at(*metadata_names) + end + + def value_with_metadata_hash(value, metadata_hash) + value_with_metadata_values(value, metadata_values_from_hash(metadata_hash)) + end + + def value_with_metadata_values(value, metadata_values) + metadata_values.dup.unshift(value) + end + + def values_to_struct(values) + if values + raise 'invalid value' unless Array === values + @struct.new(*values) + end + end + + def values_to_value(values) + if values + raise 'invalid value' unless Array === values + values.first + end + end + end +end diff --git a/spec/features/metadata.rb b/spec/features/metadata.rb new file mode 100644 index 00000000..f77bedbb --- /dev/null +++ b/spec/features/metadata.rb @@ -0,0 +1,215 @@ +shared_examples :metadata do + it 'allows instantiation using a :metadata option' do + store = new_store(metadata: [:type, :name, :thing]) + expect(store.metadata_names).to include(:type, :name, :thing) + end + + context 'with metadata fields specified' do + let(:store_with_metadata) { new_store(metadata: %i{type name}) } + + it 'ignores unknown metadata keys' do + store_with_metadata.store('x', 'y', metadata: {name: 'test', _unknown: 'test2'}) + expect(store_with_metadata.load('x', return_metadata: true)).not_to respond_to(:_unknown) + end + + describe '#create' do + it 'allows storing metadata using the :metadata option' do + expect(store_with_metadata.create('x', 'y', metadata: {name: 'test', type: 'thing'})).to be true + expect(store_with_metadata.load('x', return_metadata: true).to_h).to match a_hash_including(name: 'test', type: 'thing', value: 'y') + end + end + + describe '#delete' do + it 'returns a struct containing the value and metadata if :return_metadata is true' do + store_with_metadata.store('x', 'q', metadata: {name: 'testing'}) + struct = store_with_metadata.delete('x', return_metadata: true) + + expect(store_with_metadata.key?('x')).to be false + expect(struct).to be_a Struct + expect(struct.to_h).to match a_hash_including(name: 'testing', value: 'q', type: nil) + end + end + + describe '#fetch_values' do + it 'returns an array of structs if :return_metadata is true' do + store_with_metadata.store('x', '1', metadata: {name: 'test1'}) + store_with_metadata.store('y', '2', metadata: {name: 'test2'}) + + structs = store_with_metadata.fetch_values('x', 'y', 'z', return_metadata: true) + expect(structs[0..1]).to all be_a Struct + expect(structs[2]).to be nil + expect(structs[0..1].map(&:to_h)).to match([ + a_hash_including(value: '1', name: 'test1', type: nil), + a_hash_including(value: '2', name: 'test2', type: nil) + ]) + end + + it 'yields missing keys to the block, and uses the return values with nil metadata' do + store_with_metadata.store('x', '1', metadata: {name: 'test1'}) + store_with_metadata.store('y', '2', metadata: {name: 'test2'}) + + structs = store_with_metadata.fetch_values('x', 'y', 'z', return_metadata: true) do |key| + expect(key).to eq 'z' + 'yielded value' + end + + expect(structs).to all be_a Struct + expect(structs.map(&:to_h)).to match([ + a_hash_including(value: '1', name: 'test1', type: nil), + a_hash_including(value: '2', name: 'test2', type: nil), + a_hash_including(value: 'yielded value', name: nil, type: nil) + ]) + end + end + + describe '#load' do + it 'returns a struct if the key is found and :return_metadata is true' do + store_with_metadata.store('x', 'q', metadata: {name: 'testing'}) + struct = store_with_metadata.load('x', return_metadata: true) + expect(struct).to be_a Struct + expect(struct.to_h).to match a_hash_including(value: 'q', name: 'testing', type: nil) + end + + it 'returns nil if the key is not found and :return_metadata is true' do + struct = store_with_metadata.load('x', return_metadata: true) + expect(struct).to be nil + end + end + + describe '#merge!' do + it 'accepts a :metadata option, which allows assigning the same metadata to all values' do + store_with_metadata.merge!( + { + 'x' => '1', + 'y' => '2' + }, + metadata: { + name: 'test1', + type: 'q' + } + ) + + expect(store_with_metadata.values_at('x', 'y', return_metadata: true).map(&:to_h)).to match([ + a_hash_including(value: '1', name: 'test1', type: 'q'), + a_hash_including(value: '2', name: 'test1', type: 'q') + ]) + end + + it 'accepts a :yield_metadata option, which causes structs to be yielded to the given block' do + block = double('block') + + expect(block).to receive(:call) do |key, old_struct, new_struct| + expect(key).to eq('x') + expect([old_struct, new_struct]).to all be_a Struct + expect(old_struct.to_h).to match a_hash_including(value: '3', name: 'some test', type: nil) + expect(new_struct.to_h).to match a_hash_including(value: '1', name: nil, type: nil) + + new_struct.type = 'test' + new_struct + end + + store_with_metadata.store('x', '3', metadata: {name: 'some test'}) + expect(store_with_metadata.merge!( + { + 'x' => '1', + 'y' => '2' + }, + yield_metadata: true, + &block.method(:call) + )).to be store_with_metadata + + expect(store_with_metadata.load('x', return_metadata: true).to_h).to include(name: nil, type: 'test') + end + + it 'accepts both :metadata and :yield_metadata options together' do + block = double('block') + + expect(block).to receive(:call) do |key, old_struct, new_struct| + expect(key).to eq('x') + expect([old_struct, new_struct]).to all be_a Struct + expect(old_struct.to_h).to match a_hash_including(value: '3', name: 'some test', type: nil) + expect(new_struct.to_h).to match a_hash_including(value: '1', name: 'testing', type: 'z') + + new_struct.type = 'r' + new_struct + end + + store_with_metadata.store('x', '3', metadata: {name: 'some test'}) + expect(store_with_metadata.merge!( + { + 'x' => '1', + 'y' => '2' + }, + metadata: { + name: 'testing', + type: 'z' + }, + yield_metadata: true, + &block.method(:call) + )).to be store_with_metadata + + expect(store_with_metadata.load('x', return_metadata: true).to_h).to include(name: 'testing', type: 'r') + end + end + + describe '#slice' do + it 'accepts a :return_metadata option, which causes structs to be returned' do + store_with_metadata.store('x', '1', metadata: { name: 'test x', type: 'w' }) + store_with_metadata.store('y', '2', metadata: { name: 'test y', type: 'z' }) + + pairs = store_with_metadata.slice('x', 'y', 'z', return_metadata: true) + hash = Hash[pairs] + + expect(hash.values).to all be_a Struct + expect(hash.transform_values(&:to_h)).to match( + 'x' => a_hash_including( + value: '1', + name: 'test x', + type: 'w' + ), + 'y' => a_hash_including( + value: '2', + name: 'test y', + type: 'z' + ) + ) + end + end + + describe '#store' do + it 'accepts a :metadata option, allowing metadata to be stored' do + expect(store_with_metadata.store('x', '1', metadata: { name: 'test' })).to eq '1' + expect(store_with_metadata.load('x', return_metadata: true).to_h).to match a_hash_including( + value: '1', + name: 'test', + type: nil + ) + end + + it 'accepts a :return_metadata option, casuing metadata to be returned in a struct' do + struct = store_with_metadata.store('x', '1', return_metadata: true) + expect(struct).to be_a Struct + expect(struct.to_h).to match a_hash_including( + value: '1', + name: nil, + type: nil + ) + end + end + + describe '#values_at' do + it 'returns an array of structs if :return_metadata is true' do + store_with_metadata.store('x', '1', metadata: {name: 'test1'}) + store_with_metadata.store('y', '2', metadata: {name: 'test2'}) + + structs = store_with_metadata.values_at('x', 'y', 'z', return_metadata: true) + expect(structs[0..1]).to all be_a Struct + expect(structs[2]).to be nil + expect(structs[0..1].map(&:to_h)).to match([ + a_hash_including(value: '1', name: 'test1', type: nil), + a_hash_including(value: '2', name: 'test2', type: nil) + ]) + end + end + end +end diff --git a/spec/features/transform_value_expires.rb b/spec/features/transform_value_expires.rb index 3e9ab115..542c1592 100644 --- a/spec/features/transform_value_expires.rb +++ b/spec/features/transform_value_expires.rb @@ -1,41 +1,25 @@ shared_examples :transform_value_expires do it 'allows to bypass transformer with :raw' do store['key'] = 'value' - load_value(store.load('key', raw: true)).should == 'value' + expect(load_value(store.load('key', raw: true))).to eq ['value', nil] store['key'] = [1,2,3] - load_value(store.load('key', raw: true)).should == [[1,2,3]] + expect(load_value(store.load('key', raw: true))).to eq [[1,2,3], nil] store['key'] = nil - load_value(store.load('key', raw: true)).should == [nil] + expect(load_value(store.load('key', raw: true))).to eq [nil, nil] store['key'] = false - load_value(store.load('key', raw: true)).should be false + expect(load_value(store.load('key', raw: true))).to eq [false, nil] store.store('key', 'value', expires: 10) - load_value(store.load('key', raw: true)).first.should == 'value' - load_value(store.load('key', raw: true)).last.should respond_to(:to_int) + expect(load_value(store.load('key', raw: true)).first).to eq 'value' + expect(load_value(store.load('key', raw: true)).last).to respond_to(:to_int) store.store('key', 'value', raw: true) - store.load('key', raw: true).should == 'value' - store.delete('key', raw: true).should == 'value' + expect(store.load('key', raw: true)).to eq 'value' + expect(store.delete('key', raw: true)).to eq 'value' end it 'returns unmarshalled value' do store.store('key', 'unmarshalled value', raw: true) - store.load('key', raw: true).should == 'unmarshalled value' - end - - it 'might raise exception on invalid value' do - store.store('key', 'unmarshalled value', raw: true) - - begin - store['key'].should == load_value('unmarshalled value') - store.delete('key').should == load_value('unmarshalled value') - rescue Exception => ex - expect do - store['key'] - end.to raise_error - expect do - store.delete('key') - end.to raise_error - end + expect(store.load('key', raw: true)).to eq 'unmarshalled value' end end diff --git a/spec/helper.rb b/spec/helper.rb index 79f49368..fd645b2a 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -82,7 +82,7 @@ class MonetaSpecs def initialize(options = {}) @specs = options.delete(:specs).to_a - @features = @specs & [:expires, :expires_native, :increment, :each_key, :create] + @features = @specs & [:expires, :expires_native, :increment, :each_key, :create, :metadata] @key = options.delete(:key) || %w(object string binary hash boolean nil integer float) @value = options.delete(:value) || %w(object string binary hash boolean nil integer float) end @@ -205,6 +205,10 @@ def with_each_key def without_create new(specs: specs - [:create, :concurrent_create, :create_expires] + [:not_create]) end + + def with_metadata + new(specs: specs | %i(metadata)) + end end ADAPTER_SPECS = MonetaSpecs.new( @@ -235,9 +239,11 @@ module ClassMethods def moneta_store store_name, options={}, &block name = self.description - builder = proc do + builder = proc do |**proc_options| if block - options = instance_exec(&block) + options = instance_exec(**proc_options, &block) + else + options.merge!(proc_options) end Moneta.new(store_name, options.merge(logger: {file: File.join(tempdir, "#{name}.log")})) @@ -310,8 +316,8 @@ def tempdir @moneta_tempdir ||= Dir.mktmpdir end - def new_store - instance_eval(&@moneta_store_builder) + def new_store(**options) + instance_exec(**options, &@moneta_store_builder) end def store diff --git a/spec/moneta/adapters/gdbm/standard_gdbm_with_expires_spec.rb b/spec/moneta/adapters/gdbm/standard_gdbm_with_expires_spec.rb index ca507924..ff8d9d4a 100644 --- a/spec/moneta/adapters/gdbm/standard_gdbm_with_expires_spec.rb +++ b/spec/moneta/adapters/gdbm/standard_gdbm_with_expires_spec.rb @@ -3,9 +3,10 @@ let(:min_ttl) { t_res } use_timecop - moneta_store :GDBM do - {file: File.join(tempdir, "simple_gdbm_with_expires"), expires: true} + moneta_store :GDBM do |metadata: nil, **options| + p metadata + {file: File.join(tempdir, "simple_gdbm_with_expires"), expires: true, metadata: metadata} end - moneta_specs STANDARD_SPECS.without_multiprocess.with_expires.with_each_key + moneta_specs STANDARD_SPECS.without_multiprocess.with_expires.with_each_key.with_metadata end diff --git a/spec/moneta/proxies/expires/expires_file_spec.rb b/spec/moneta/proxies/expires/expires_file_spec.rb index 267bedd8..f1df36ff 100644 --- a/spec/moneta/proxies/expires/expires_file_spec.rb +++ b/spec/moneta/proxies/expires/expires_file_spec.rb @@ -4,16 +4,16 @@ use_timecop - moneta_build do + moneta_build do |metadata: nil, **options| tempdir = self.tempdir Moneta.build do - use :Expires + use :Expires, metadata: metadata use :Transformer, key: [:marshal, :escape], value: :marshal adapter :File, dir: File.join(tempdir, "expires-file") end end - moneta_specs STANDARD_SPECS.with_expires.stringvalues_only.with_each_key + moneta_specs STANDARD_SPECS.with_expires.stringvalues_only.with_each_key.with_metadata it 'deletes expired value in underlying file storage' do store.store('foo', 'bar', expires: 2) diff --git a/spec/moneta/proxies/expires/expires_memory_spec.rb b/spec/moneta/proxies/expires/expires_memory_spec.rb index b8c66647..718d1aa2 100644 --- a/spec/moneta/proxies/expires/expires_memory_spec.rb +++ b/spec/moneta/proxies/expires/expires_memory_spec.rb @@ -4,12 +4,19 @@ use_timecop - moneta_build do + moneta_build do |metadata: nil, **options| Moneta.build do - use :Expires - adapter :Memory + use :Expires, metadata: metadata + adapter :Memory, options end end - moneta_specs STANDARD_SPECS.without_transform.with_expires.without_persist.returnsame.with_each_key + moneta_specs STANDARD_SPECS.without_transform.with_expires.without_persist.returnsame.with_each_key.with_metadata + + it 'raises an exception when loading an invalid value' do + store.store('key', 'unmarshalled value', raw: true) + + expect { store['key'] }.to raise_error + expect { store.delete('key') }.to raise_error + end end diff --git a/spec/moneta/proxies/expires/expires_memory_with_default_expires_spec.rb b/spec/moneta/proxies/expires/expires_memory_with_default_expires_spec.rb index 48e7b0f1..5bcaf870 100644 --- a/spec/moneta/proxies/expires/expires_memory_with_default_expires_spec.rb +++ b/spec/moneta/proxies/expires/expires_memory_with_default_expires_spec.rb @@ -4,13 +4,13 @@ use_timecop - moneta_build do + moneta_build do |metadata: nil, **options| min_ttl = self.min_ttl Moneta.build do - use :Expires, expires: min_ttl + use :Expires, expires: min_ttl, metadata: metadata adapter :Memory end end - moneta_specs STANDARD_SPECS.without_transform.with_expires.with_default_expires.without_persist.returnsame.with_each_key + moneta_specs STANDARD_SPECS.without_transform.with_expires.with_default_expires.without_persist.returnsame.with_each_key.with_metadata end diff --git a/spec/moneta/proxies/metadata/metadata_memory_spec.rb b/spec/moneta/proxies/metadata/metadata_memory_spec.rb new file mode 100644 index 00000000..fe81252b --- /dev/null +++ b/spec/moneta/proxies/metadata/metadata_memory_spec.rb @@ -0,0 +1,18 @@ +describe 'metadata_memory', proxy: :Metadata do + moneta_build do |metadata: %i{test1 test2}, **options| + Moneta.build do + use :Metadata, metadata: metadata + adapter :Memory, options + end + end + + moneta_specs STANDARD_SPECS.without_transform.without_persist.returnsame.with_each_key.with_metadata + + it 'raises an exception when loading an invalid value' do + store.store('key', 'unmarshalled value', raw: true) + + expect { store['key'] }.to raise_error + expect { store.delete('key') }.to raise_error + end +end +