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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b8947f56d293a6a55ae10dcdc009bfa6b2cfbd4
4
- data.tar.gz: 75825ca40a5002cd3c1506b77df310aa6cb9138b
3
+ metadata.gz: 4447e24f3730702d1e8941a42376605fa905064d
4
+ data.tar.gz: 9adf5320bb7fe9c8f1e8f719c54146fe8fade703
5
5
  SHA512:
6
- metadata.gz: c41df44791461e8d1471a7d8a6567f3677cda98cb0b2a4226108cfc31adcb3feed3f894e6b0347a3881de61674149c478b40b4f320a3124a55d2d3df70e60a99
7
- data.tar.gz: ff9bfc17e13950b8bb5844d625d4e8c1912221c0c46423250f569a4a5c0e438e0a39b27875e03c64e06d02852b2e2d3d985dc8b7d1d7814ff7bfda66de96fb2f
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, [ :female, :male ]
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 # => :female
28
- user.gender_male? # => false
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.gender_male? # => true
40
+ user.gender_female? # => false
33
41
  user.gender_raw # => 1
34
42
 
35
- User.genders # => { :female => 0, :male => 1 }
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, :default => 0
52
+ t.integer :gender, default: 0
46
53
  end
47
54
  ```
48
55
 
49
- 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.
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.where(gender: User.genders(:female))
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 custom validation options to `validates_inclusion_of`.
84
+ You can pass `validates: true` to enable validation.
72
85
 
73
86
  ```ruby
74
- enum_accessor :status, [ :on, :off ], validation_options: { message: "incorrect status" }
87
+ enum_accessor :status, [:on, :off], validates: true
75
88
  ```
76
89
 
77
- Or skip validation entirely.
90
+ You can also pass validation options.
78
91
 
79
92
  ```ruby
80
- enum_accessor :status, [ :on, :off ], validate: false
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.human_genders # => { :female => '女', :male => '男' }
114
+ user.human_gender # => '女'
115
+ User.genders.human_dict # => { 'female' => '女', 'male' => '男' }
103
116
 
104
117
  I18n.locale = :en
105
- user.human_gender # => 'Female'
106
- User.human_genders # => { :female => 'Female', :male => 'Male' }
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 each one of them has incompatible design policies than EnumAccessor.
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(field, enums, options={})
10
- # Normalize arguments
11
- field = field.to_s
12
- case enums
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(field) do
29
- symbolized_enums.key(read_attribute(field))
15
+ define_method(column) do
16
+ send(definition).dict.key(read_attribute(column))
30
17
  end
31
18
 
32
19
  # Setter
33
- define_method("#{field}=") do |arg|
20
+ define_method("#{column}=") do |arg|
34
21
  case arg
35
22
  when String, Symbol
36
- write_attribute field, const[arg.to_s]
37
- when Integer
38
- write_attribute field, arg
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("#{field}_raw") do
44
- read_attribute field
30
+ define_method("#{column}_raw") do
31
+ read_attribute(column)
45
32
  end
46
33
 
47
- # Raw-value setter
48
- define_method("#{field}_raw=") do |arg|
49
- write_attribute field, Integer(arg)
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
- # Checker
53
- symbolized_enums.keys.each do |key|
54
- method_name = key.to_s.downcase.gsub(/[-\s]/, '_')
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 method
61
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
62
- def self.#{field.pluralize}(symbol = nil)
63
- return #{symbolized_enums} if symbol.nil?
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
- def self.human_#{field.pluralize}(symbol = nil)
68
- humanized_enums = Hash[#{symbolized_enums}.map{|k,v| [k, human_enum_accessor(:#{field}, k)] }]
69
- return humanized_enums if symbol.nil?
70
- return humanized_enums[symbol]
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
- EOS
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
- # Human-friendly print
75
- define_method("human_#{field}") do
76
- self.class.human_enum_accessor(field, self.send(field))
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
- unless options[:validate] == false
81
- validates_inclusion_of field, { :in => symbolized_enums.keys }.merge(options[:validation_options] || {})
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
- # Mimics ActiveModel::Translation.human_attribute_name
86
- def human_enum_accessor(field, key, options = {})
87
- defaults = lookup_ancestors.map do |klass|
88
- :"#{self.i18n_scope}.enum_accessor.#{klass.model_name.i18n_key}.#{field}.#{key}"
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
- options.reverse_merge! :count => 1, :default => defaults
96
- I18n.translate(defaults.shift, options)
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
@@ -1,3 +1,3 @@
1
1
  module EnumAccessor
2
- VERSION = '0.3.0'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -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, [ :female, :male ]
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(:female)
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.human_genders(:female)).to eq('女')
46
- expect(User.human_genders).to eq({ :female => '女', :male => '男' })
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.human_genders(:female)).to eq('Female')
52
- expect(User.human_genders).to eq({ :female => 'Female', :male => 'Male' })
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 'defines internal constant' do
56
- expect(User::GENDERS).to eq({ "female" => 0, "male" => 1 })
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 'adds class methods' do
60
- expect(User.genders).to eq({ :female => 0, :male => 1 })
61
- expect(User.genders[:female]).to eq(0)
62
- expect(User.genders(:female)).to eq(0)
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
- @user.gender = 'bogus'
67
- expect(@user.valid?).to be_falsey
68
-
69
- @user.gender = 'male'
70
- expect(@user.valid?).to be_truthy
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(:female))
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.3.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-20 00:00:00.000000000 Z
11
+ date: 2014-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport