store_field 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +6 -0
- data/lib/store_field.rb +42 -0
- data/lib/store_field/railtie.rb +11 -0
- data/lib/store_field/version.rb +3 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/store_field_spec.rb +56 -0
- data/store_field.gemspec +24 -0
- metadata +107 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Kenn Ejima
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# StoreField - Nested fields for ActiveRecord::Store
|
2
|
+
|
3
|
+
Rails 3.2 introduced [ActiveRecord::Store](http://api.rubyonrails.org/classes/ActiveRecord/Store.html), which offers simple single-column key-value stores.
|
4
|
+
|
5
|
+
It's a nice feature, but its accessors are limited to primitive values (e.g. `String`, `Integer`, etc.) and it doesn't work out of the box if you want to store structured values. (e.g. `Hash`, `Set`, etc.)
|
6
|
+
|
7
|
+
Here's an example.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class User < ActiveRecord::Base
|
11
|
+
store :options, accessors: [ :tutorials, :preference ]
|
12
|
+
end
|
13
|
+
|
14
|
+
user = User.new
|
15
|
+
user.tutorials[:quick_start] = :visited # => NoMethodError: undefined method `[]=' for nil:NilClass
|
16
|
+
```
|
17
|
+
|
18
|
+
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.
|
19
|
+
|
20
|
+
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.
|
21
|
+
|
22
|
+
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!
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'store_field'
|
30
|
+
```
|
31
|
+
|
32
|
+
Define `store_field` in a model class, following the `store` method.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class User < ActiveRecord::Base
|
36
|
+
store :storage
|
37
|
+
store_field :tutorials
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Now the previous example works perfectly.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
user = User.new
|
45
|
+
user.tutorials[:quick_start] = :finished
|
46
|
+
```
|
47
|
+
|
48
|
+
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.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
store_field :tutorials, in: :storage, type: Hash
|
52
|
+
```
|
53
|
+
|
54
|
+
## Typing support for Set
|
55
|
+
|
56
|
+
In addition to `Hash`, StoreField supports the `Set` data type. To use Set, simply pass `type: Set` option.
|
57
|
+
|
58
|
+
It turns out that Set is extremely useful most of the time when you think what you need is `Array`.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
store_field :funnel, type: Set
|
62
|
+
```
|
63
|
+
|
64
|
+
It defines several utility methods - `set_[field]`, `unset_[field]`, `set_[field]?` and `unset_[field]?`.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
cart = Cart.new
|
68
|
+
cart.funnel # => #<Set: {}>
|
69
|
+
cart.set_funnel(:add_item)
|
70
|
+
cart.set_funnel(:checkout)
|
71
|
+
cart.set_funnel?(:checkout) # => true
|
72
|
+
cart.funnel # => #<Set: {:add_item, :checkout}>
|
73
|
+
```
|
74
|
+
|
75
|
+
`set_[field]` and `unset_[field]` return `self`, so you can call `save` in chain.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
cart.set_funnel(:checkout).save! # => true
|
79
|
+
```
|
80
|
+
|
81
|
+
## Use cases for the Set type
|
82
|
+
|
83
|
+
Set is a great way to store an arbitrary number of states.
|
84
|
+
|
85
|
+
Consider you have a system that sends an alert when some criteria have been met.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
if user.bandwidth_usage > 250.megabytes
|
89
|
+
Email.to user, message: 'Your data plan usage is nearing 300MB limit'
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
Depending on at what time the above code gets run (daily, hourly, etc.), email could be sent multiple times. To prevent duplicate alerts, you need to store the state in the database when one is successfully delivered.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class User < ActiveRecord::Base
|
97
|
+
store :storage
|
98
|
+
store_field :delivered, type: Set
|
99
|
+
end
|
100
|
+
|
101
|
+
if user.bandwidth_usage > 250.megabytes and !user.set_delivered?(:nearing_limit)
|
102
|
+
Email.to user, message: 'Your data plan usage is nearing 300MB limit'
|
103
|
+
user.set_delivered(:nearing_limit).save
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
That way, the user won't receive the same alert again, until `unset_delivered` is called when the next billing cycle starts.
|
data/Rakefile
ADDED
data/lib/store_field.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'store_field/version'
|
2
|
+
require 'store_field/railtie'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module StoreField
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def store_field(key, options = {})
|
10
|
+
raise ArgumentError.new(':in is invalid') if options[:in] and serialized_attributes[options[:in].to_s].nil?
|
11
|
+
raise ArgumentError.new(':type is invalid') if options[:type] and ![ Hash, Set ].include?(options[:type])
|
12
|
+
|
13
|
+
store_attribute = options[:in] || serialized_attributes.keys.first
|
14
|
+
raise ArgumentError.new('store method must be defined before store_field') if store_attribute.nil?
|
15
|
+
|
16
|
+
# Accessor
|
17
|
+
define_method(key) do
|
18
|
+
value = send("#{store_attribute}")[key]
|
19
|
+
if value.nil?
|
20
|
+
value = store_field_init(options[:type])
|
21
|
+
send("#{store_attribute}")[key] = value
|
22
|
+
end
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
# Utility methods for Set
|
27
|
+
if options[:type] == Set
|
28
|
+
define_method("set_#{key}") {|value| send(key).add(value); self }
|
29
|
+
define_method("unset_#{key}") {|value| send(key).delete(value); self }
|
30
|
+
define_method("set_#{key}?") {|value| send(key).include?(value) }
|
31
|
+
define_method("unset_#{key}?") {|value| !send(key).include?(value) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def store_field_init(klass)
|
37
|
+
return {} unless klass
|
38
|
+
klass.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ActiveRecord::Base.send(:include, StoreField)
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'store_field'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
ActiveRecord::Base.send(:include, StoreField)
|
8
|
+
|
9
|
+
# Establish in-memory database connection
|
10
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
11
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
ActiveRecord::Base.connection.create_table :users, force: true do |t|
|
4
|
+
t.text :storage
|
5
|
+
end
|
6
|
+
|
7
|
+
class User < ActiveRecord::Base
|
8
|
+
store :storage
|
9
|
+
store_field :preference
|
10
|
+
store_field :count_caches
|
11
|
+
store_field :notified, type: Set
|
12
|
+
store_field :displayed, type: Set
|
13
|
+
store_field :funnel, type: Set
|
14
|
+
end
|
15
|
+
|
16
|
+
describe StoreField do
|
17
|
+
before do
|
18
|
+
@user = User.new
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises when store is not defined beforehand' do
|
22
|
+
expect { Class.new(ActiveRecord::Base) { store :storage; store_field :notified } }.to_not raise_error(ArgumentError)
|
23
|
+
expect { Class.new(ActiveRecord::Base) { store_field :notified } }.to raise_error(ArgumentError)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'raises when invalid option is given' do
|
27
|
+
expect { Class.new(ActiveRecord::Base) { store :storage; store_field :notified, type: File } }.to raise_error(ArgumentError)
|
28
|
+
expect { Class.new(ActiveRecord::Base) { store :storage; store_field :notified, in: :bogus } }.to raise_error(ArgumentError)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'initializes with the specified type' do
|
32
|
+
@user.preference.should == {}
|
33
|
+
@user.notified.should == Set.new
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets and unsets keywords' do
|
37
|
+
@user.set_notified(:welcome)
|
38
|
+
@user.set_notified(:first_deposit)
|
39
|
+
|
40
|
+
# Consume balance, notify once and only once
|
41
|
+
@user.set_notified(:balance_low)
|
42
|
+
@user.set_notified(:balance_negative)
|
43
|
+
|
44
|
+
# Another deposit, restore balance
|
45
|
+
@user.unset_notified(:balance_low)
|
46
|
+
@user.unset_notified(:balance_negative)
|
47
|
+
|
48
|
+
@user.notified.should == Set.new([:welcome, :first_deposit])
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'saves in-line' do
|
52
|
+
@user.set_notified(:welcome).save.should == true
|
53
|
+
@user.reload
|
54
|
+
@user.set_notified?(:welcome).should == true
|
55
|
+
end
|
56
|
+
end
|
data/store_field.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'store_field/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'store_field'
|
8
|
+
gem.version = StoreField::VERSION
|
9
|
+
gem.authors = ['Kenn Ejima']
|
10
|
+
gem.email = ['kenn.ejima@gmail.com']
|
11
|
+
gem.description = %q{Nested fields for ActiveRecord::Store}
|
12
|
+
gem.summary = %q{Nested fields for ActiveRecord::Store}
|
13
|
+
gem.homepage = 'https://github.com/kenn/store_field'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'activerecord', '>= 3.2.0'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
gem.add_development_dependency 'sqlite3'
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: store_field
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kenn Ejima
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sqlite3
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Nested fields for ActiveRecord::Store
|
63
|
+
email:
|
64
|
+
- kenn.ejima@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- Gemfile
|
72
|
+
- LICENSE.txt
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/store_field.rb
|
76
|
+
- lib/store_field/railtie.rb
|
77
|
+
- lib/store_field/version.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/store_field_spec.rb
|
80
|
+
- store_field.gemspec
|
81
|
+
homepage: https://github.com/kenn/store_field
|
82
|
+
licenses: []
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.24
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Nested fields for ActiveRecord::Store
|
105
|
+
test_files:
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- spec/store_field_spec.rb
|