Skip to content

Commit

Permalink
Adding :values option support for Set. v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kenn committed Sep 21, 2012
1 parent c3bef01 commit d8539f9
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 14 deletions.
18 changes: 15 additions & 3 deletions README.md
Expand Up @@ -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

Expand Down Expand Up @@ -78,9 +78,21 @@ cart.funnel # => #<Set: {:add_item, :checkout}>
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.

Expand Down
16 changes: 11 additions & 5 deletions lib/store_field.rb
Expand Up @@ -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) }
Expand Down
2 changes: 1 addition & 1 deletion lib/store_field/version.rb
@@ -1,3 +1,3 @@
module StoreField
VERSION = '1.0.0'
VERSION = '1.1.0'
end
13 changes: 8 additions & 5 deletions spec/store_field_spec.rb
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit d8539f9

Please sign in to comment.