diff --git a/README.md b/README.md index ea16f82..1136200 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ user.tutorials[:quick_start] = :visited # => NoMethodError: undefined method There are two ways to solve this problem - a. break down `options` into multiple columns like `tutorials` and `preference`, or b. define an accessor method for each to initialize with an empty `Hash` when accessed for the first time. -The former is bad because the TEXT (or BLOB) column type could be [stored off-page](http://www.mysqlperformanceblog.com/2010/02/09/blob-storage-in-innodb/) when it gets big and you could hit some strange bugs and/or performance penalty. Furthermore, adding columns kills the primary purpose of having key-value store - you use this feature because you don't like migrations, right? So it's two-fold bad. +The former is bad because the TEXT (or BLOB) column type could be stored off-page when it gets big and you could hit some [strange bugs and/or performance penalty](http://www.mysqlperformanceblog.com/2010/02/09/blob-storage-in-innodb/). Furthermore, adding columns kills the primary purpose of having key-value store - you use this feature because you don't like migrations, right? So it's two-fold bad. -StoreField takes the latter approach. It defines accessors that initializes with an empty `Hash` or `Set` automatically. Now you have a single TEXT column for everything! +StoreField takes the latter approach. It defines accessors that initialize with an empty `Hash` or `Set` automatically. Now you have a single TEXT column for everything! ## Usage @@ -78,9 +78,21 @@ cart.funnel # => # cart.set_funnel(:checkout).save! # => true ``` +Also you can enumerate acceptable values, which is validated on the `set_[field]` method. + +```ruby +store_field :funnel, type: Set, values: [ :add_item, :checkout ] +``` + +With the definition above, the following code will raise an exception. + +```ruby +set_funnel(:bogus) # => ArgumentError: :bogus is not allowed +``` + ## Use cases for the Set type -Set is a great way to store an arbitrary number of states. +Set is a great way to store an arbitrary number of named states. Consider you have a system that sends an alert when some criteria have been met. diff --git a/lib/store_field.rb b/lib/store_field.rb index f1e5889..f9a40eb 100644 --- a/lib/store_field.rb +++ b/lib/store_field.rb @@ -7,26 +7,32 @@ module StoreField module ClassMethods def store_field(key, options = {}) - raise ArgumentError.new(':in is invalid') if options[:in] and serialized_attributes[options[:in].to_s].nil? - raise ArgumentError.new(':type is invalid') if options[:type] and ![ Hash, Set ].include?(options[:type]) + raise ArgumentError.new(':in is invalid') if options[:in] and serialized_attributes[options[:in].to_s].nil? + raise ArgumentError.new(':type is invalid') if options[:type] and ![ Hash, Set ].include?(options[:type]) + raise ArgumentError.new(':values is invalid') if options[:values] and !options[:values].is_a?(Array) klass = options[:type] + values = options[:values] store_attribute = options[:in] || serialized_attributes.keys.first raise ArgumentError.new('store method must be defined before store_field') if store_attribute.nil? # Accessor define_method(key) do - value = send("#{store_attribute}")[key] + value = send(store_attribute)[key] if value.nil? value = klass ? klass.new : {} - send("#{store_attribute}")[key] = value + send(store_attribute)[key] = value end value end # Utility methods for Set if options[:type] == Set - define_method("set_#{key}") {|value| send(key).add(value); self } + define_method("set_#{key}") do |value| + raise ArgumentError.new("#{value.inspect} is not allowed") if values and !values.include?(value) + send(key).add(value) + self + end define_method("unset_#{key}") {|value| send(key).delete(value); self } define_method("set_#{key}?") {|value| send(key).include?(value) } define_method("unset_#{key}?") {|value| !send(key).include?(value) } diff --git a/lib/store_field/version.rb b/lib/store_field/version.rb index c41aec5..52785c2 100644 --- a/lib/store_field/version.rb +++ b/lib/store_field/version.rb @@ -1,3 +1,3 @@ module StoreField - VERSION = '1.0.0' + VERSION = '1.1.0' end diff --git a/spec/store_field_spec.rb b/spec/store_field_spec.rb index 4e28c1b..c30e563 100644 --- a/spec/store_field_spec.rb +++ b/spec/store_field_spec.rb @@ -7,7 +7,7 @@ class User < ActiveRecord::Base store :storage store_field :tutorials - store_field :delivered, type: Set + store_field :delivered, type: Set, values: [ :welcome, :balance_low ] end describe StoreField do @@ -30,19 +30,22 @@ class User < ActiveRecord::Base @user.delivered.should == Set.new end + it 'raises when invalid value is given for Set' do + expect { + @user.set_delivered(:bogus) + }.to raise_error(ArgumentError) + end + it 'sets and unsets keywords' do @user.set_delivered(:welcome) - @user.set_delivered(:first_deposit) # Consume balance, notify once and only once @user.set_delivered(:balance_low) - @user.set_delivered(:balance_negative) # Another deposit, restore balance @user.unset_delivered(:balance_low) - @user.unset_delivered(:balance_negative) - @user.delivered.should == Set.new([:welcome, :first_deposit]) + @user.delivered.should == Set.new([:welcome]) end it 'saves in-line' do