Skip to content

Commit

Permalink
Hash-type supports keys option. Set-type with values option now adds …
Browse files Browse the repository at this point in the history
…a validation rather than raising an exception. Version bump to v2.0.0
  • Loading branch information
kenn committed Nov 18, 2012
1 parent 9a3ed70 commit b99f511
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 46 deletions.
48 changes: 35 additions & 13 deletions README.md
Expand Up @@ -21,6 +21,10 @@ The former is bad because the TEXT (or BLOB) column type could be stored off-pag


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! 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!


Changelog:

* v2.0.0: Hash-type supports `keys` option to add accessors for validation. Set-type with `values` option now adds a validation rather than raising an exception.

## Usage ## Usage


Add this line to your application's Gemfile. Add this line to your application's Gemfile.
Expand All @@ -45,17 +49,34 @@ user = User.new
user.tutorials[:quick_start] = :finished user.tutorials[:quick_start] = :finished
``` ```


When no option is given, it defaults to the first serialized column, using the `Hash` datatype. So `store_field :tutorials` is equivalent to the following. When no option is given, it defaults to the first serialized column, using Hash-type. So `store_field :tutorials` is equivalent to the following.


```ruby ```ruby
store_field :tutorials, in: :storage, type: Hash store_field :tutorials, in: :storage, type: Hash
``` ```


## Typing support for Set ## Hash-type features

When the `keys` option is given for Hash-type, convenience accessors are automatically defined, which can be used for validation.


In addition to `Hash`, StoreField supports the `Set` data type. To use Set, simply pass `type: Set` option. ```ruby
class User < ActiveRecord::Base
store_field :tutorials, keys: [ :quick_start ]


It turns out that Set is extremely useful most of the time when you think what you need is `Array`. validates :tutorials_quick_start, inclusion: { in: [ :started, :finished ], allow_nil: true }
end

user = User.new
user.tutorials_quick_start = :started
user.valid?
=> true
```

## Set-type features

In addition to Hash-type, StoreField supports Set-type. To use Set-type, simply pass `type: Set` option.

It turns out that Set-type is extremely useful most of the time when you think what you need is `Array`.


```ruby ```ruby
store_field :funnel, type: Set store_field :funnel, type: Set
Expand All @@ -78,21 +99,22 @@ cart.funnel # => #<Set: {:add_item, :checkout}>
cart.set_funnel(:checkout).save! # => true cart.set_funnel(:checkout).save! # => true
``` ```


Also you can enumerate acceptable values, which will be validated in the `set_[field]` method. Also you can enumerate acceptable values for validation.


```ruby ```ruby
store_field :funnel, type: Set, values: [ :add_item, :checkout ] class Cart < ActiveRecord::Base
``` store_field :funnel, type: Set, values: [ :add_item, :checkout ]

end
With the definition above, the following code will raise an exception.


```ruby cart = Cart.new
set_funnel(:bogus) # => ArgumentError: :bogus is not allowed cart.set_funnel(:bogus)
cart.valid?
=> false
``` ```


## Use cases for the Set type ## Use cases for the Set-type


Set is a great way to store an arbitrary number of named states. Set-type 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. Consider you have a system that sends an alert when some criteria have been met.


Expand Down
42 changes: 29 additions & 13 deletions lib/store_field.rb
Expand Up @@ -7,35 +7,51 @@ module StoreField


module ClassMethods module ClassMethods
def store_field(key, options = {}) def store_field(key, options = {})
raise ArgumentError.new(':in is invalid') if options[:in] and serialized_attributes[options[:in].to_s].nil? raise ArgumentError, ':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, ':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] klass = options[:type] || Hash
values = options[:values]
store_attribute = options[:in] || serialized_attributes.keys.first store_attribute = options[:in] || serialized_attributes.keys.first
raise ArgumentError.new('store method must be defined before store_field') if store_attribute.nil? raise ScriptError, 'store method must be defined before store_field' if store_attribute.nil?


# Accessor # Accessor
define_method(key) do define_method(key) do
value = send(store_attribute)[key] value = send(store_attribute)[key]
if value.nil? if value.nil?
value = klass ? klass.new : {} value = klass.new
send(store_attribute)[key] = value send(store_attribute)[key] = value
end end
value value
end end


# Utility methods for Set # Utility methods for Hash
if options[:type] == Set if klass == Hash and options[:keys]
define_method("set_#{key}") do |value| options[:keys].each do |subkey|
raise ArgumentError.new("#{value.inspect} is not allowed") if values and !values.include?(value) define_method("#{key}_#{subkey}") do
send(key).add(value) send(key)[subkey]
self end

define_method("#{key}_#{subkey}=") do |value|
send(key)[subkey] = value
end
end end
end

# Utility methods for Set
if klass == Set
define_method("set_#{key}") {|value| send(key).add(value); self }
define_method("unset_#{key}") {|value| send(key).delete(value); self } define_method("unset_#{key}") {|value| send(key).delete(value); self }
define_method("set_#{key}?") {|value| send(key).include?(value) } define_method("set_#{key}?") {|value| send(key).include?(value) }
define_method("unset_#{key}?") {|value| !send(key).include?(value) } define_method("unset_#{key}?") {|value| !send(key).include?(value) }

if options[:values]
validate do
diff = send(key).to_a - options[:values]
unless diff.empty?
errors.add(key, "is invalid with #{diff.inspect}")
end
end
end
end end
end end
end end
Expand Down
2 changes: 1 addition & 1 deletion lib/store_field/version.rb
@@ -1,3 +1,3 @@
module StoreField module StoreField
VERSION = '1.1.0' VERSION = '2.0.0'
end end
59 changes: 40 additions & 19 deletions spec/store_field_spec.rb
Expand Up @@ -6,8 +6,10 @@


class User < ActiveRecord::Base class User < ActiveRecord::Base
store :storage store :storage
store_field :tutorials store_field :tutorials, keys: [ :quick_start ]
store_field :delivered, type: Set, values: [ :welcome, :balance_low ] store_field :delivered, type: Set, values: [ :welcome, :balance_low ]

validates :tutorials_quick_start, inclusion: { in: [ :started, :finished ], allow_nil: true }
end end


describe StoreField do describe StoreField do
Expand All @@ -16,8 +18,8 @@ class User < ActiveRecord::Base
end end


it 'raises when store is not defined beforehand' do it 'raises when store is not defined beforehand' do
expect { Class.new(ActiveRecord::Base) { store :storage; store_field :delivered } }.to_not raise_error(ArgumentError) expect { Class.new(ActiveRecord::Base) { store :storage; store_field :delivered } }.to_not raise_error(ScriptError)
expect { Class.new(ActiveRecord::Base) { store_field :delivered } }.to raise_error(ArgumentError) expect { Class.new(ActiveRecord::Base) { store_field :delivered } }.to raise_error(ScriptError)
end end


it 'raises when invalid option is given' do it 'raises when invalid option is given' do
Expand All @@ -28,29 +30,48 @@ class User < ActiveRecord::Base
it 'initializes with the specified type' do it 'initializes with the specified type' do
@user.tutorials.should == {} @user.tutorials.should == {}
@user.delivered.should == Set.new @user.delivered.should == Set.new
@user.valid?.should == true
end end


it 'raises when invalid value is given for Set' do describe Hash do
expect { it 'validates Hash' do
@user.set_delivered(:bogus) @user.tutorials_quick_start = :started
}.to raise_error(ArgumentError) @user.valid?.should == true
@user.errors.empty?.should == true

@user.tutorials_quick_start = :bogus
@user.valid?.should == false
@user.errors.has_key?(:tutorials_quick_start).should == true
end
end end


it 'sets and unsets keywords' do describe Set do
@user.set_delivered(:welcome) it 'validates Set' do
@user.set_delivered(:welcome)
@user.valid?.should == true
@user.errors.empty?.should == true


# Consume balance, notify once and only once @user.set_delivered(:bogus)
@user.set_delivered(:balance_low) @user.valid?.should == false
@user.errors.has_key?(:delivered).should == true
end


# Another deposit, restore balance it 'sets and unsets keywords' do
@user.unset_delivered(:balance_low) @user.set_delivered(:welcome)


@user.delivered.should == Set.new([:welcome]) # Consume balance, notify once and only once
end @user.set_delivered(:balance_low)

# Another deposit, restore balance
@user.unset_delivered(:balance_low)

@user.delivered.should == Set.new([:welcome])
end


it 'saves in-line' do it 'saves in-line' do
@user.set_delivered(:welcome).save.should == true @user.set_delivered(:welcome).save.should == true
@user.reload @user.reload
@user.set_delivered?(:welcome).should == true @user.set_delivered?(:welcome).should == true
end
end end
end end

0 comments on commit b99f511

Please sign in to comment.