forked from lwe/simple_enum
/
simple_enum.rb
247 lines (223 loc) · 10.8 KB
/
simple_enum.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# SimpleEnum allows for cross-database, easy to use enum-like fields to be added to your
# ActiveRecord models. It does not rely on database specific column types like <tt>ENUM</tt> (MySQL),
# but instead on integer columns.
#
# Author:: Lukas Westermann
# Copyright:: Copyright (c) 2009 Lukas Westermann (Zurich, Switzerland)
# Licence:: MIT-Licence (http://www.opensource.org/licenses/mit-license.php)
#
# See the +as_enum+ documentation for more details.
# because we depend on AR and i18n
require 'active_record'
require 'i18n'
require 'simple_enum/enum_hash'
require 'simple_enum/object_support'
require 'simple_enum/validation'
# Base module which gets included in <tt>ActiveRecord::Base</tt>. See documentation
# of +SimpleEnum::ClassMethods+ for more details.
module SimpleEnum
# +SimpleEnum+ version string.
VERSION = "1.3.0".freeze
class << self
# Provides configurability to SimpleEnum, allows to override some defaults which are
# defined for all uses of +as_enum+. Most options from +as_enum+ are available, such as:
# * <tt>:prefix</tt> - Define a prefix, which is prefixed to the shortcut methods (e.g. <tt><symbol>!</tt> and
# <tt><symbol>?</tt>), if it's set to <tt>true</tt> the enumeration name is used as a prefix, else a custom
# prefix (symbol or string) (default is <tt>nil</tt> => no prefix)
# * <tt>:slim</tt> - If set to <tt>true</tt> no shortcut methods for all enumeration values are being generated, if
# set to <tt>:class</tt> only class-level shortcut methods are disabled (default is <tt>nil</tt> => they are generated)
# * <tt>:upcase</tt> - If set to +true+ the <tt>Klass.foos</tt> is named <tt>Klass.FOOS</tt>, why? To better suite some
# coding-styles (default is +false+ => downcase)
# * <tt>:whiny</tt> - Boolean value which if set to <tt>true</tt> will throw an <tt>ArgumentError</tt>
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
# <tt>false</tt> no exception is thrown and the internal value is set to <tt>nil</tt> (default is <tt>true</tt>)
def default_options
@default_options ||= {
:whiny => true,
:upcase => false
}
end
def included(base) #:nodoc:
base.send :extend, ClassMethods
end
end
module ClassMethods
# Provides ability to create simple enumerations based on hashes or arrays, backed
# by integer columns (but not limited to integer columns).
#
# Columns are supposed to be suffixed by <tt>_cd</tt>, if not, use <tt>:column => 'the_column_name'</tt>,
# so some example migrations:
#
# add_column :users, :gender_cd, :integer
# add_column :users, :status, :integer # and a custom column...
#
# and then in your model:
#
# class User < ActiveRecord::Base
# as_enum :gender, [:male, :female]
# end
#
# # or use a hash:
#
# class User < ActiveRecord::Base
# as_enum :status, { :active => 1, :inactive => 0, :archived => 2, :deleted => 3 }, :column => 'status'
# end
#
# Now it's possible to access the enumeration and the internally stored value like:
#
# john_doe = User.new
# john_doe.gender # => nil
# john_doe.gender = :male
# john_doe.gender # => :male
# john_doe.gender_cd # => 0
#
# And to make life a tad easier: a few shortcut methods to work with the enumeration are also created.
#
# john_doe.male? # => true
# john_doe.female? # => false
# john_doe.female! # => :female (set's gender to :female => gender_cd = 1)
# john_doe.male? # => false
#
# Sometimes it's required to access the db-backed values, like e.g. in a query:
#
# User.genders # => { :male => 0, :female => 1}, values hash
# User.genders(:male) # => 0, value access (via hash)
# User.female # => 1, direct access
# User.find :all, :conditions => { :gender_cd => User.female } # => [...], list with all women
#
# To access the key/value assocations in a helper like the select helper or similar use:
#
# <%= select(:user, :gender, User.genders.keys)
#
# The generated shortcut methods (like <tt>male?</tt> or <tt>female!</tt> etc.) can also be prefixed
# using the <tt>:prefix</tt> option. If the value is <tt>true</tt>, the shortcut methods are prefixed
# with the name of the enumeration.
#
# class User < ActiveRecord::Base
# as_enum :gender, [:male, :female], :prefix => true
# end
#
# jane_doe = User.new
# jane_doe.gender = :female # this is still as-is
# jane_doe.gender_cd # => 1, and so it this
#
# jane_doe.gender_female? # => true (instead of jane_doe.female?)
#
# It is also possible to supply a custom prefix.
#
# class Item < ActiveRecord::Base
# as_enum :status, [:inactive, :active, :deleted], :prefix => :state
# end
#
# item = Item.new(:status => :active)
# item.state_inactive? # => false
# Item.state_deleted # => 2
# Item.status(:deleted) # => 2, same as above...
#
# To disable the generation of the shortcut methods for all enumeration values, add <tt>:slim => true</tt> to
# the options.
#
# class Address < ActiveRecord::Base
# as_enum :canton, {:aargau => 'ag', ..., :wallis => 'vs', :zug => 'zg', :zurich => 'zh'}, :slim => true
# end
#
# home = Address.new(:canton => :zurich, :street => 'Bahnhofstrasse 1', ...)
# home.canton # => :zurich
# home.canton_cd # => 'zh'
# home.aargau! # throws NoMethodError: undefined method `aargau!'
# Address.aargau # throws NoMethodError: undefined method `aargau`
#
# This is especially useful if there are (too) many enumeration values, or these shortcut methods
# are not required.
#
# === Configuration options:
# * <tt>:column</tt> - Specifies a custom column name, instead of the default suffixed <tt>_cd</tt> column
# * <tt>:prefix</tt> - Define a prefix, which is prefixed to the shortcut methods (e.g. <tt><symbol>!</tt> and
# <tt><symbol>?</tt>), if it's set to <tt>true</tt> the enumeration name is used as a prefix, else a custom
# prefix (symbol or string) (default is <tt>nil</tt> => no prefix)
# * <tt>:slim</tt> - If set to <tt>true</tt> no shortcut methods for all enumeration values are being generated, if
# set to <tt>:class</tt> only class-level shortcut methods are disabled (default is <tt>nil</tt> => they are generated)
# * <tt>:upcase</tt> - If set to +true+ the <tt>Klass.foos</tt> is named <tt>Klass.FOOS</tt>, why? To better suite some
# coding-styles (default is +false+ => downcase)
# * <tt>:whiny</tt> - Boolean value which if set to <tt>true</tt> will throw an <tt>ArgumentError</tt>
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
# <tt>false</tt> no exception is thrown and the internal value is set to <tt>nil</tt> (default is <tt>true</tt>)
def as_enum(enum_cd, values, options = {})
options = SimpleEnum.default_options.merge({ :column => "#{enum_cd}_cd" }).merge(options)
options.assert_valid_keys(:column, :whiny, :prefix, :slim, :upcase)
# convert array to hash...
values = SimpleEnum::EnumHash.new(values)
values_inverted = values.invert
# store info away
write_inheritable_attribute(:enum_definitions, {}) if enum_definitions.nil?
enum_definitions[enum_cd] = enum_definitions[options[:column]] = { :name => enum_cd, :column => options[:column], :options => options }
# generate getter
define_method("#{enum_cd}") do
id = read_attribute options[:column]
values_inverted[id]
end
# generate setter
define_method("#{enum_cd}=") do |new_value|
v = new_value.blank? ? nil : values[new_value.to_sym]
raise(ArgumentError, "Invalid enumeration value: #{new_value}") if (options[:whiny] and v.nil? and !new_value.blank?)
write_attribute options[:column], v
end
# allow access to defined values hash, e.g. in a select helper or finder method.
attr_name = enum_cd.to_s.pluralize
enum_attr = :"#{attr_name.downcase}_enum_hash"
write_inheritable_attribute(enum_attr, values)
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def self.#{attr_name}(*args)
return read_inheritable_attribute(#{enum_attr.inspect}) if args.first.nil?
return read_inheritable_attribute(#{enum_attr.inspect})[args.first] if args.size == 1
args.inject([]) { |ary, sym| ary << read_inheritable_attribute(#{enum_attr.inspect})[sym]; ary }
end
def self.#{attr_name}_for_select(&block)
self.#{attr_name}.map do |k,v|
[block_given? ? yield(k,v) : self.human_enum_name(#{attr_name.inspect}, k), k]
end.sort
end
RUBY
# only create if :slim is not defined
if options[:slim] != true
# create both, boolean operations and *bang* operations for each
# enum "value"
prefix = options[:prefix] && "#{options[:prefix] == true ? enum_cd : options[:prefix]}_"
values.each do |k,code|
sym = k.to_enum_sym
define_method("#{prefix}#{sym}?") do
code == read_attribute(options[:column])
end
define_method("#{prefix}#{sym}!") do
write_attribute options[:column], code
sym
end
# allow class access to each value
unless options[:slim] === :class
singleton_class.send(:define_method, "#{prefix}#{sym}", Proc.new { |*args| args.first ? k : code })
end
end
end
end
include Validation
def human_enum_name(enum, key, options = {})
klasses = self.respond_to?(:descendants) ? descendants : ancestors
defaults = ([self] + klasses).map { |klass| :"#{klass.name.underscore}.#{enum}.#{key}" }
defaults << :"#{enum}.#{key}"
defaults << options.delete(:default) if options[:default]
defaults << "#{key}".humanize
options[:count] ||= 1
I18n.translate(defaults.shift, options.merge(:default => defaults.flatten, :scope => [:activerecord, :enums]))
end
protected
# Returns enum definitions as defined by each call to
# +as_enum+.
def enum_definitions
read_inheritable_attribute(:enum_definitions)
end
end
end
# Tie stuff together and load translations if ActiveRecord is defined
Object.send(:include, SimpleEnum::ObjectSupport)
ActiveRecord::Base.send(:include, SimpleEnum)
I18n.load_path << File.join(File.dirname(__FILE__), '..', 'locales', 'en.yml')