Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Hash-type supports keys option. Set-type with values option now adds …

…a validation rather than raising an exception. Version bump to v2.0.0
  • Loading branch information...
commit b99f51100028d0a4f2b3b6a83796a739c05a5a27 1 parent 9a3ed70
@kenn authored
View
48 README.md
@@ -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!
+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
Add this line to your application's Gemfile.
@@ -45,17 +49,34 @@ user = User.new
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
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
store_field :funnel, type: Set
@@ -78,21 +99,22 @@ cart.funnel # => #<Set: {:add_item, :checkout}>
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
-store_field :funnel, type: Set, values: [ :add_item, :checkout ]
-```
-
-With the definition above, the following code will raise an exception.
+class Cart < ActiveRecord::Base
+ store_field :funnel, type: Set, values: [ :add_item, :checkout ]
+end
-```ruby
-set_funnel(:bogus) # => ArgumentError: :bogus is not allowed
+cart = Cart.new
+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.
View
42 lib/store_field.rb
@@ -7,35 +7,51 @@ 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(':values is invalid') if options[:values] and !options[:values].is_a?(Array)
+ raise ArgumentError, ':in is invalid' if options[:in] and serialized_attributes[options[:in].to_s].nil?
+ raise ArgumentError, ':type is invalid' if options[:type] and ![ Hash, Set ].include?(options[:type])
- klass = options[:type]
- values = options[:values]
+ klass = options[:type] || Hash
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
define_method(key) do
value = send(store_attribute)[key]
if value.nil?
- value = klass ? klass.new : {}
+ value = klass.new
send(store_attribute)[key] = value
end
value
end
- # Utility methods for Set
- if options[:type] == Set
- 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
+ # Utility methods for Hash
+ if klass == Hash and options[:keys]
+ options[:keys].each do |subkey|
+ define_method("#{key}_#{subkey}") do
+ send(key)[subkey]
+ end
+
+ define_method("#{key}_#{subkey}=") do |value|
+ send(key)[subkey] = value
+ 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("set_#{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
View
2  lib/store_field/version.rb
@@ -1,3 +1,3 @@
module StoreField
- VERSION = '1.1.0'
+ VERSION = '2.0.0'
end
View
59 spec/store_field_spec.rb
@@ -6,8 +6,10 @@
class User < ActiveRecord::Base
store :storage
- store_field :tutorials
+ store_field :tutorials, keys: [ :quick_start ]
store_field :delivered, type: Set, values: [ :welcome, :balance_low ]
+
+ validates :tutorials_quick_start, inclusion: { in: [ :started, :finished ], allow_nil: true }
end
describe StoreField do
@@ -16,8 +18,8 @@ class User < ActiveRecord::Base
end
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_field :delivered } }.to 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(ScriptError)
end
it 'raises when invalid option is given' do
@@ -28,29 +30,48 @@ class User < ActiveRecord::Base
it 'initializes with the specified type' do
@user.tutorials.should == {}
@user.delivered.should == Set.new
+ @user.valid?.should == true
end
- it 'raises when invalid value is given for Set' do
- expect {
- @user.set_delivered(:bogus)
- }.to raise_error(ArgumentError)
+ describe Hash do
+ it 'validates Hash' do
+ @user.tutorials_quick_start = :started
+ @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
- it 'sets and unsets keywords' do
- @user.set_delivered(:welcome)
+ describe Set do
+ 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(:balance_low)
+ @user.set_delivered(:bogus)
+ @user.valid?.should == false
+ @user.errors.has_key?(:delivered).should == true
+ end
- # Another deposit, restore balance
- @user.unset_delivered(:balance_low)
+ it 'sets and unsets keywords' do
+ @user.set_delivered(:welcome)
- @user.delivered.should == Set.new([:welcome])
- end
+ # Consume balance, notify once and only once
+ @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
- @user.set_delivered(:welcome).save.should == true
- @user.reload
- @user.set_delivered?(:welcome).should == true
+ it 'saves in-line' do
+ @user.set_delivered(:welcome).save.should == true
+ @user.reload
+ @user.set_delivered?(:welcome).should == true
+ end
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.