enum_accessor 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +39 -35
- data/lib/enum_accessor.rb +67 -60
- data/lib/enum_accessor/version.rb +1 -1
- data/spec/enum_accessor_spec.rb +60 -23
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4447e24f3730702d1e8941a42376605fa905064d
|
4
|
+
data.tar.gz: 9adf5320bb7fe9c8f1e8f719c54146fe8fade703
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30990654b79c136e12443358a3fae8470d91e0966affe68b8d93a9f65df7cbfd372c4f6c4100345aa7597f7521d03221d991a80a5ea3485077b81f5417063434
|
7
|
+
data.tar.gz: a308687d6221dab4b912d65e28be73266d9fdb90e0cc6b2591a4a2c47a00dc27537288e728688422030e2ef8dfaed0e681fc70eb09cd081368db8ba836a6416a
|
data/README.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
EnumAccessor lets you define enum for attributes, and store them as integer in the database.
|
4
4
|
|
5
|
+
It is very similar to [Official Rails 4.1 Implementation](http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums), but EnumAccessor offers quite a few advantages:
|
6
|
+
|
7
|
+
* Safe predicate methods (`user.status_active?` instead of `user.active?`)
|
8
|
+
* Validation
|
9
|
+
* Scope
|
10
|
+
* Translation
|
11
|
+
|
5
12
|
Compatible with ActiveRecord 3 or later.
|
6
13
|
|
7
14
|
## Usage
|
@@ -16,7 +23,7 @@ Define `enum_accessor` in a model class.
|
|
16
23
|
|
17
24
|
```ruby
|
18
25
|
class User < ActiveRecord::Base
|
19
|
-
enum_accessor :gender, [
|
26
|
+
enum_accessor :gender, [:female, :male]
|
20
27
|
end
|
21
28
|
```
|
22
29
|
|
@@ -24,16 +31,16 @@ And now you have a set of methods and constants.
|
|
24
31
|
|
25
32
|
```ruby
|
26
33
|
user = User.new
|
27
|
-
user.gender
|
28
|
-
user.
|
34
|
+
user.gender = 'female' # Takes String or Symbol
|
35
|
+
user.gender # => "female"
|
36
|
+
user.gender_female? # => true
|
29
37
|
user.gender_raw # => 0
|
30
38
|
|
31
39
|
user.gender = :male
|
32
|
-
user.
|
40
|
+
user.gender_female? # => false
|
33
41
|
user.gender_raw # => 1
|
34
42
|
|
35
|
-
User.genders
|
36
|
-
User::GENDERS # => { "female" => 0, "male" => 1 }
|
43
|
+
User.genders.dict # => { 'female' => 0, 'male' => 1 }
|
37
44
|
```
|
38
45
|
|
39
46
|
Notice that zero-based numbering is used as database values.
|
@@ -42,11 +49,11 @@ Your migration should look like this.
|
|
42
49
|
|
43
50
|
```ruby
|
44
51
|
create_table :users do |t|
|
45
|
-
t.integer :gender, :
|
52
|
+
t.integer :gender, default: 0
|
46
53
|
end
|
47
54
|
```
|
48
55
|
|
49
|
-
Optionally, it would be a good idea to add
|
56
|
+
Optionally, it would be a good idea to add `limit: 1` on the column for even better space efficiency when the enum set is small.
|
50
57
|
|
51
58
|
## Manual coding
|
52
59
|
|
@@ -63,21 +70,27 @@ enum_accessor :status, ok: 200, not_found: 404, internal_server_error: 500
|
|
63
70
|
For querying purpose, use `User.genders` method to retrieve internal integer values.
|
64
71
|
|
65
72
|
```ruby
|
66
|
-
User.
|
73
|
+
User.where_gender(:female)
|
74
|
+
```
|
75
|
+
|
76
|
+
Also takes multiple values.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
User.where_status(:active, :pending)
|
67
80
|
```
|
68
81
|
|
69
82
|
## Validations
|
70
83
|
|
71
|
-
You can pass
|
84
|
+
You can pass `validates: true` to enable validation.
|
72
85
|
|
73
86
|
```ruby
|
74
|
-
enum_accessor :status, [
|
87
|
+
enum_accessor :status, [:on, :off], validates: true
|
75
88
|
```
|
76
89
|
|
77
|
-
|
90
|
+
You can also pass validation options.
|
78
91
|
|
79
92
|
```ruby
|
80
|
-
enum_accessor :status, [
|
93
|
+
enum_accessor :status, [:on, :off], validates: { allow_nil: true }
|
81
94
|
```
|
82
95
|
|
83
96
|
## Translation
|
@@ -98,40 +111,31 @@ and now `human_*` methods return a translated string. It defaults to `humanize`
|
|
98
111
|
|
99
112
|
```ruby
|
100
113
|
I18n.locale = :ja
|
101
|
-
user.human_gender
|
102
|
-
User.
|
114
|
+
user.human_gender # => '女'
|
115
|
+
User.genders.human_dict # => { 'female' => '女', 'male' => '男' }
|
103
116
|
|
104
117
|
I18n.locale = :en
|
105
|
-
user.human_gender
|
106
|
-
User.
|
118
|
+
user.human_gender # => 'Female'
|
119
|
+
User.genders.human_dict # => { 'female' => 'Female', 'male' => 'Male' }
|
107
120
|
```
|
108
121
|
|
109
122
|
## Changelog
|
110
123
|
|
124
|
+
- v1.0.0:
|
125
|
+
- Drop support for Ruby 1.8
|
126
|
+
- Now getter method returns a String rather than a Symbol
|
127
|
+
- Do not validate by default
|
128
|
+
- Added `where_gender(:female)` scope
|
129
|
+
- Removed the `_raw=` setter as it is now aware of the argument type
|
130
|
+
- Removed constants (e.g. `User::GENDERS`) and now use the class attribute to save the definition
|
111
131
|
- v0.3.0: Add support for `find_or_create_by`
|
112
132
|
|
113
|
-
## Why enum keys are internally stored as strings rather than symbols?
|
114
|
-
|
115
|
-
Because `params[:gender].to_sym` is dangerous. It could lead to problems like memory leak, slow symbol table lookup, or even DoS attack. If a user sends random strings for the parameter, it generates uncontrollable number of symbols, which can never be garbage collected, and eventually causes `symbol table overflow (RuntimeError)`, eating up gigabytes of memory.
|
116
|
-
|
117
|
-
For the same reason, `ActiveSupport::HashWithIndifferentAccess` (which is used for `params`) keeps hash keys as string internally.
|
118
|
-
|
119
|
-
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/hash_with_indifferent_access.rb
|
120
|
-
|
121
133
|
## Other solutions
|
122
134
|
|
123
|
-
There are tons of similar gems out there. Then why did I bother creating another one myself rather than sending pull requests to one of them? Because
|
135
|
+
There are tons of similar gems out there. Then why did I bother creating another one myself rather than sending pull requests to one of them? Because most of them define enum values as top-level predicate methods, which can cause method conflict. (`user.active?` vs `user.status_active?`)
|
124
136
|
|
137
|
+
* [Official Rails 4.1 Implementation](http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums)
|
125
138
|
* [simple_enum](https://github.com/lwe/simple_enum)
|
126
|
-
* Pretty close to EnumAccessor feature-wise but requires `*_cd` suffix for the database column, which makes AR scopes ugly.
|
127
|
-
* Enum values are defined as top-level predicate methods, which could conflict with existing methods. Also you can't define multiple enums to the same model. In some use cases, predicate methods are not necessary and you just want to be on the safe side.
|
128
|
-
* [enumerated_attribute](https://github.com/jeffp/enumerated_attribute)
|
129
|
-
* Top-level predicate methods. Many additional methods are coupled with a specific usage assumption.
|
130
|
-
* [enum_field](https://github.com/jamesgolick/enum_field)
|
131
|
-
* Top-level predicate methods.
|
132
|
-
* [coded_options](https://github.com/jasondew/coded_options)
|
133
|
-
* [active_enum](https://github.com/adzap/active_enum)
|
134
|
-
* [classy_enum](https://github.com/beerlington/classy_enum)
|
135
139
|
* [enumerize](https://github.com/brainspec/enumerize)
|
136
140
|
|
137
141
|
Also, EnumAccessor has one of the simplest code base, so that you can easily hack on.
|
data/lib/enum_accessor.rb
CHANGED
@@ -6,94 +6,101 @@ module EnumAccessor
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
-
def enum_accessor(
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
when Array
|
14
|
-
enums = Hash[enums.map.with_index{|v,i| [v.to_s, i] }]
|
15
|
-
when Hash
|
16
|
-
enums = Hash[enums.map{|k,v| [k.to_s, v] }]
|
17
|
-
else
|
18
|
-
raise ArgumentError.new('enum_accessor takes Array or Hash as the second argument')
|
19
|
-
end
|
20
|
-
|
21
|
-
const_name = field.pluralize.upcase
|
22
|
-
const_set(const_name, enums) unless const_defined?(const_name)
|
23
|
-
const = const_get(const_name)
|
24
|
-
|
25
|
-
symbolized_enums = Hash[enums.map{|k,v| [k.to_sym, v] }]
|
9
|
+
def enum_accessor(column, keys, options={})
|
10
|
+
definition = :"#{column}_enum_accessor"
|
11
|
+
class_attribute definition # will set instance accessor as well
|
12
|
+
send "#{definition}=", Definition.new(column, keys, self)
|
26
13
|
|
27
14
|
# Getter
|
28
|
-
define_method(
|
29
|
-
|
15
|
+
define_method(column) do
|
16
|
+
send(definition).dict.key(read_attribute(column))
|
30
17
|
end
|
31
18
|
|
32
19
|
# Setter
|
33
|
-
define_method("#{
|
20
|
+
define_method("#{column}=") do |arg|
|
34
21
|
case arg
|
35
22
|
when String, Symbol
|
36
|
-
write_attribute
|
37
|
-
when Integer
|
38
|
-
write_attribute
|
23
|
+
write_attribute column, send(definition).dict[arg]
|
24
|
+
when Integer, NilClass
|
25
|
+
write_attribute column, arg
|
39
26
|
end
|
40
27
|
end
|
41
28
|
|
42
29
|
# Raw-value getter
|
43
|
-
define_method("#{
|
44
|
-
read_attribute
|
30
|
+
define_method("#{column}_raw") do
|
31
|
+
read_attribute(column)
|
45
32
|
end
|
46
33
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
34
|
+
# Predicate
|
35
|
+
send(definition).dict.each do |key, int|
|
36
|
+
define_method("#{column}_#{key}?") do
|
37
|
+
read_attribute(column) == int
|
38
|
+
end
|
50
39
|
end
|
51
40
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
define_method("#{field}_#{method_name}?") do
|
56
|
-
self.send(field) == key
|
57
|
-
end
|
41
|
+
# Human-friendly print
|
42
|
+
define_method("human_#{column}") do
|
43
|
+
self.class.send "human_#{column}", send(column)
|
58
44
|
end
|
59
45
|
|
60
|
-
# Class
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
return #{symbolized_enums}[symbol]
|
65
|
-
end
|
46
|
+
# Class methods
|
47
|
+
define_singleton_method column.to_s.pluralize do
|
48
|
+
send(definition)
|
49
|
+
end
|
66
50
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
51
|
+
# Human-friendly print on class level
|
52
|
+
# Mimics ActiveModel::Translation.human_attribute_name
|
53
|
+
define_singleton_method "human_#{column}" do |key, options={}|
|
54
|
+
defaults = lookup_ancestors.map do |klass|
|
55
|
+
:"#{self.i18n_scope}.enum_accessor.#{klass.model_name.i18n_key}.#{column}.#{key}"
|
71
56
|
end
|
72
|
-
|
57
|
+
defaults << :"enum_accessor.#{self.model_name.i18n_key}.#{column}.#{key}"
|
58
|
+
defaults << :"enum_accessor.#{column}.#{key}"
|
59
|
+
defaults << options.delete(:default) if options[:default]
|
60
|
+
defaults << key.to_s.humanize
|
73
61
|
|
74
|
-
|
75
|
-
|
76
|
-
|
62
|
+
options.reverse_merge! count: 1, default: defaults
|
63
|
+
I18n.translate(defaults.shift, options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Scopes
|
67
|
+
define_singleton_method "where_#{column}" do |*args|
|
68
|
+
integers = args.map{|arg| send(definition).dict[arg] }.compact
|
69
|
+
where(column => integers)
|
77
70
|
end
|
78
71
|
|
79
72
|
# Validation
|
80
|
-
|
81
|
-
|
73
|
+
if options.has_key?(:validate) or options.has_key?(:validation_options)
|
74
|
+
raise ArgumentError, 'validation options are updated. please refer to the documentation.'
|
75
|
+
end
|
76
|
+
if options[:validates]
|
77
|
+
validation_options = options[:validates].is_a?(Hash) ? options[:validates] : {}
|
78
|
+
validates column, { inclusion: { in: send(definition).dict.keys } }.merge(validation_options)
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
82
|
+
class Definition
|
83
|
+
attr_accessor :dict
|
84
|
+
|
85
|
+
def initialize(column, keys, klass)
|
86
|
+
dict = case keys
|
87
|
+
when Array
|
88
|
+
Hash[keys.map.with_index{|i,index| [i, index] }]
|
89
|
+
when Hash
|
90
|
+
keys
|
91
|
+
else
|
92
|
+
raise ArgumentError.new('enum_accessor takes Array or Hash as the second argument')
|
93
|
+
end
|
94
|
+
|
95
|
+
@column = column
|
96
|
+
@klass = klass
|
97
|
+
@dict = dict.with_indifferent_access.freeze
|
89
98
|
end
|
90
|
-
defaults << :"enum_accessor.#{self.model_name.i18n_key}.#{field}.#{key}"
|
91
|
-
defaults << :"enum_accessor.#{field}.#{key}"
|
92
|
-
defaults << options.delete(:default) if options[:default]
|
93
|
-
defaults << key.to_s.humanize
|
94
99
|
|
95
|
-
|
96
|
-
|
100
|
+
def human_dict
|
101
|
+
# Don't memoize - I18n.locale can change
|
102
|
+
Hash[@dict.keys.map{|key| [key, @klass.send("human_#{@column}", key)] }].with_indifferent_access
|
103
|
+
end
|
97
104
|
end
|
98
105
|
end
|
99
106
|
end
|
data/spec/enum_accessor_spec.rb
CHANGED
@@ -7,7 +7,7 @@ ActiveRecord::Base.connection.create_table :users, force: true do |t|
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class User < ActiveRecord::Base
|
10
|
-
enum_accessor :gender, [
|
10
|
+
enum_accessor :gender, [:female, :male]
|
11
11
|
end
|
12
12
|
|
13
13
|
describe EnumAccessor do
|
@@ -15,66 +15,103 @@ describe EnumAccessor do
|
|
15
15
|
@user = User.new
|
16
16
|
end
|
17
17
|
|
18
|
+
after do
|
19
|
+
User.delete_all
|
20
|
+
end
|
21
|
+
|
18
22
|
it 'adds checker' do
|
19
23
|
expect(@user.gender_female?).to eq(true)
|
20
24
|
expect(@user.gender_male?).to eq(false)
|
21
25
|
end
|
22
26
|
|
23
27
|
it 'adds getter' do
|
24
|
-
expect(@user.gender).to eq(
|
28
|
+
expect(@user.gender).to eq('female')
|
25
29
|
end
|
26
30
|
|
27
31
|
it 'adds setter' do
|
28
32
|
@user.gender = :male
|
29
33
|
expect(@user.gender_male?).to eq(true)
|
34
|
+
|
35
|
+
@user.gender = nil
|
36
|
+
expect(@user.gender.nil?).to eq(true)
|
30
37
|
end
|
31
38
|
|
32
39
|
it 'adds raw value getter' do
|
33
40
|
expect(@user.gender_raw).to eq(0)
|
34
41
|
end
|
35
42
|
|
36
|
-
it 'adds raw value setter' do
|
37
|
-
@user.gender_raw = 1
|
38
|
-
expect(@user.gender_male?).to eq(true)
|
39
|
-
end
|
40
|
-
|
41
43
|
it 'adds humanized methods' do
|
42
44
|
I18n.locale = :ja
|
43
45
|
expect(User.human_attribute_name(:gender)).to eq('性別')
|
44
46
|
expect(@user.human_gender).to eq('女')
|
45
|
-
expect(User.
|
46
|
-
expect(User.
|
47
|
+
expect(User.genders.human_dict[:female]).to eq('女')
|
48
|
+
expect(User.genders.human_dict).to eq({ 'female' => '女', 'male' => '男' })
|
47
49
|
|
48
50
|
I18n.locale = :en
|
49
51
|
expect(User.human_attribute_name(:gender)).to eq('Gender')
|
50
52
|
expect(@user.human_gender).to eq('Female')
|
51
|
-
expect(User.
|
52
|
-
expect(User.
|
53
|
+
expect(User.genders.human_dict[:female]).to eq('Female')
|
54
|
+
expect(User.genders.human_dict).to eq({ 'female' => 'Female', 'male' => 'Male' })
|
53
55
|
end
|
54
56
|
|
55
|
-
it '
|
56
|
-
expect(User
|
57
|
+
it 'adds class methods' do
|
58
|
+
expect(User.genders.dict).to eq({ 'female' => 0, 'male' => 1 })
|
59
|
+
expect(User.genders.dict[:female]).to eq(0)
|
57
60
|
end
|
58
61
|
|
59
|
-
it '
|
60
|
-
|
61
|
-
|
62
|
-
|
62
|
+
it 'supports manual coding' do
|
63
|
+
class UserManualCoding < ActiveRecord::Base
|
64
|
+
self.table_name = :users
|
65
|
+
enum_accessor :gender, female: 100, male: 200
|
66
|
+
end
|
67
|
+
|
68
|
+
user = UserManualCoding.new
|
69
|
+
user.gender = :male
|
70
|
+
expect(user.gender_male?).to eq(true)
|
71
|
+
expect(user.gender_raw).to eq(200)
|
63
72
|
end
|
64
73
|
|
65
74
|
it 'adds validation' do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
class UserValidate < ActiveRecord::Base
|
76
|
+
self.table_name = :users
|
77
|
+
enum_accessor :gender, [:female, :male], validates: true
|
78
|
+
end
|
79
|
+
|
80
|
+
class UserValidateAllowNil < ActiveRecord::Base
|
81
|
+
self.table_name = :users
|
82
|
+
enum_accessor :gender, [:female, :male], validates: { allow_nil: true }
|
83
|
+
end
|
84
|
+
|
85
|
+
user = UserValidate.new
|
86
|
+
user.gender = 'male'
|
87
|
+
expect(user.valid?).to be_truthy
|
88
|
+
user.gender = nil
|
89
|
+
expect(user.valid?).to be_falsey
|
90
|
+
user.gender = 'bogus' # Becomes nil
|
91
|
+
expect(user.valid?).to be_falsey
|
92
|
+
|
93
|
+
user = UserValidateAllowNil.new
|
94
|
+
user.gender = 'male'
|
95
|
+
expect(user.valid?).to be_truthy
|
96
|
+
user.gender = nil
|
97
|
+
expect(user.valid?).to be_truthy
|
98
|
+
user.gender = 'bogus' # Becomes nil
|
99
|
+
expect(user.valid?).to be_truthy
|
71
100
|
end
|
72
101
|
|
73
102
|
it 'supports find_or_create_by' do
|
74
103
|
# `find_or_create_by` uses where-based raw value for find,
|
75
104
|
# then passes the raw value to the setter method for create.
|
76
105
|
expect {
|
77
|
-
User.find_or_create_by(gender: User.genders
|
106
|
+
User.find_or_create_by(gender: User.genders.dict[:female])
|
78
107
|
}.to change{ User.count }.by(1)
|
79
108
|
end
|
109
|
+
|
110
|
+
it 'supports scope' do
|
111
|
+
user = User.create!(gender: :female)
|
112
|
+
|
113
|
+
expect(User.where_gender(:female).count).to eq(1)
|
114
|
+
expect(User.where_gender(:male, :female).count).to eq(1)
|
115
|
+
expect(User.where_gender(:male).count).to eq(0)
|
116
|
+
end
|
80
117
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: enum_accessor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenn Ejima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-08-
|
11
|
+
date: 2014-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|