simple_enum 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/LICENCE +20 -0
- data/README.rdoc +182 -0
- data/Rakefile +70 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/simple_enum/array_support.rb +15 -0
- data/lib/simple_enum/enum_hash.rb +36 -0
- data/lib/simple_enum/object_support.rb +37 -0
- data/lib/simple_enum/validation.rb +37 -0
- data/lib/simple_enum/version.rb +14 -0
- data/lib/simple_enum.rb +227 -0
- data/locales/en.yml +6 -0
- data/simple_enum.gemspec +74 -0
- data/test/array_conversions_test.rb +22 -0
- data/test/class_methods_test.rb +94 -0
- data/test/enum_hash_test.rb +33 -0
- data/test/finders_test.rb +41 -0
- data/test/models.rb +8 -0
- data/test/object_backed_test.rb +67 -0
- data/test/object_support_test.rb +27 -0
- data/test/prefixes_test.rb +38 -0
- data/test/se_array_support_test.rb +32 -0
- data/test/simple_enum_test.rb +97 -0
- data/test/test_helper.rb +66 -0
- data/test/without_shortcuts_test.rb +40 -0
- metadata +91 -0
data/.gitignore
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Lukas Westermann (Zurich, Switzerland)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
= SimpleEnum - unobtrusive enum-like fields for ActiveRecord
|
2
|
+
|
3
|
+
A Rails plugin which brings easy-to-use enum-like functionality to
|
4
|
+
ActiveRecord models.
|
5
|
+
|
6
|
+
*Note*: a recent search on github for `enum` turned out, that there are many, many similar solutions.
|
7
|
+
Yet, none seem to provide so many options, but I may be biased ;)
|
8
|
+
|
9
|
+
== Quick start
|
10
|
+
|
11
|
+
Add this to a model:
|
12
|
+
|
13
|
+
class User < ActiveRecord::Base
|
14
|
+
as_enum :gender, {:female => 1, :male => 0}
|
15
|
+
end
|
16
|
+
|
17
|
+
Then create the new column using migrations:
|
18
|
+
|
19
|
+
class AddGenderColumnToUser < ActiveRecord::Migration
|
20
|
+
def self.up
|
21
|
+
add_column :users, :gender_cd, :integer
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
remove_column :users, :gender_cd
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
*Done*. Now it's possible to pull some neat tricks on the new column, yet
|
30
|
+
the original db column (+gender_cd+) is still intact and not touched by
|
31
|
+
any fancy metaclass or similar.
|
32
|
+
|
33
|
+
jane = User.new
|
34
|
+
jane.gender = :female
|
35
|
+
jane.female? # => true
|
36
|
+
jane.male? # => false
|
37
|
+
jane.gender # => :female
|
38
|
+
jane.gender_cd # => 1
|
39
|
+
|
40
|
+
Easily switch to another value using the bang methods.
|
41
|
+
|
42
|
+
joe = User.new
|
43
|
+
joe.male! # => :male
|
44
|
+
joe.gender # => :male
|
45
|
+
joe.gender_cd # => 0
|
46
|
+
|
47
|
+
There are even some neat tricks at class level, which might be
|
48
|
+
useful when creating queries, displaying option elements or similar:
|
49
|
+
|
50
|
+
User.genders # => { :male => 0, :female => 1 }
|
51
|
+
User.genders(:male) # => 0, same as User.male
|
52
|
+
User.female # => 1
|
53
|
+
User.genders.female # => 1, same as User.female or User.genders(:female)
|
54
|
+
|
55
|
+
== Wait, there's more!
|
56
|
+
* Too tired of always adding the integer values? Try:
|
57
|
+
|
58
|
+
class User < ActiveRecord::Base
|
59
|
+
as_enum :status, [:deleted, :active, :disabled] # translates to :deleted => 0, :active => 1, :disabled => 2
|
60
|
+
end
|
61
|
+
|
62
|
+
*Disclaimer*: if you _ever_ decide to reorder this array, beaware that any previous mapping is lost. So it's recommended
|
63
|
+
to create mappings (that might change) using hashes instead of arrays. For stuff like gender it might be probably perfectly
|
64
|
+
fine to use arrays though.
|
65
|
+
* Maybe you've columns named differently than the proposed <tt>{column}_cd</tt> naming scheme, feel free to use any column name
|
66
|
+
by providing an option:
|
67
|
+
|
68
|
+
class User < ActiveRecord::Base
|
69
|
+
as_enum :gender, [:male, :female], :column => 'sex'
|
70
|
+
end
|
71
|
+
|
72
|
+
* To make it easier to create dropdowns with values use:
|
73
|
+
|
74
|
+
<%= select(:user, :gender, User.genders.keys) %>
|
75
|
+
|
76
|
+
* It's possible to validate the internal enum values, just like any other ActiveRecord validation:
|
77
|
+
|
78
|
+
class User < ActiveRecord::Base
|
79
|
+
as_enum :gender, [:male, :female]
|
80
|
+
validates_as_enum :gender
|
81
|
+
end
|
82
|
+
|
83
|
+
All common options like <tt>:if</tt>, <tt>:unless</tt>, <tt>:allow_nil</tt> and <tt>:message</tt> are supported, because it just works within
|
84
|
+
the standard <tt>validates_each</tt>-loop. This validation method does not check the value of <tt>user.gender</tt>, but
|
85
|
+
instead the value of <tt>@user.gender_cd</tt>.
|
86
|
+
* If the shortcut methods (like <tt><symbol>?</tt>, <tt><symbol>!</tt> or <tt>Klass.<symbol></tt>) conflict with something in your class, it's possible to
|
87
|
+
define a prefix:
|
88
|
+
|
89
|
+
class User < ActiveRecord::Base
|
90
|
+
as_enum :gender, [:male, :female], :prefix => true
|
91
|
+
end
|
92
|
+
|
93
|
+
jane = User.new :gender => :female
|
94
|
+
jane.gender_female? # => true
|
95
|
+
User.gender_female # => 1, this also works on the class methods
|
96
|
+
|
97
|
+
The <tt>:prefix</tt> option not only takes a boolean value as an argument, but instead can also be supplied a custom
|
98
|
+
prefix (i.e. any string or symbol), so with <tt>:prefix => 'foo'</tt> all shortcut methods would look like: <tt>foo_<symbol>...</tt>
|
99
|
+
*Note*: if the <tt>:slim => true</tt> is defined, this option has no effect whatsoever (because no shortcut methods are generated).
|
100
|
+
* Sometimes it might be useful to disable the generation of the shortcut methods (<tt><symbol>?</tt>, <tt><symbol>!</tt> and <tt>Klass.<symbol></tt>), to do so just add the option <tt>:slim => true</tt>:
|
101
|
+
|
102
|
+
class User < ActiveRecord::Base
|
103
|
+
as_enum :gender, [:male, :female], :slim => true
|
104
|
+
end
|
105
|
+
|
106
|
+
jane = User.new :gender => :female
|
107
|
+
jane.female? # => throws NoMethodError: undefined method `female?'
|
108
|
+
User.male # => throws NoMethodError: undefined method `male'
|
109
|
+
|
110
|
+
Yet the setter and getter for <tt>gender</tt>, as well as the <tt>User.genders</tt> methods are still available, only all shortcut
|
111
|
+
methods for each of the enumeration values are not generated.
|
112
|
+
|
113
|
+
It's also possible to set <tt>:slim => :class</tt> which only disables the generation of any class-level shortcut method, because those
|
114
|
+
are also available via the enhanced enumeration hash:
|
115
|
+
|
116
|
+
class Message < ActiveRecord::Base
|
117
|
+
as_enum :status, { :unread => 0, :read => 1, :archived => 99}, :slim => :class
|
118
|
+
end
|
119
|
+
|
120
|
+
msg = Message.new :body => 'Hello World!', status_cd => 0
|
121
|
+
msg.read? # => false; shortuct methods on instance are still enabled
|
122
|
+
msg.status # => :unread
|
123
|
+
Message.unread # => throws NoMethodError: undefined method `unread`
|
124
|
+
Message.statuses.unread # => 0
|
125
|
+
Message.statuses.unread(true) # => :unread
|
126
|
+
|
127
|
+
* As a default an <tt>ArgumentError</tt> is raised if the user tries to set the field to an invalid enumeration value, to change this
|
128
|
+
behaviour use the <tt>:whiny</tt> option:
|
129
|
+
|
130
|
+
class User < ActiveRecord::Base
|
131
|
+
as_enum :gender, [:male, :female], :whiny => false
|
132
|
+
end
|
133
|
+
|
134
|
+
* To define any option globally, like setting <tt>:whiny</tt> to +false+, or globally enable <tt>:prefix</tt>; all default options
|
135
|
+
are stored in <tt>SimpleEnum.default_options</tt>, this hash can be easily changed in your initializers or wherever:
|
136
|
+
|
137
|
+
# e.g. setting :prefix => true (globally)
|
138
|
+
SimpleEnum.default_options[:prefix] = true
|
139
|
+
|
140
|
+
== Best practices
|
141
|
+
|
142
|
+
Searching for certain values by using the finder methods:
|
143
|
+
|
144
|
+
User.find :all, :conditions => { :gender_cd => User.female }
|
145
|
+
|
146
|
+
Working with database backed values, now assuming that there exists a +genders+ table:
|
147
|
+
|
148
|
+
class Person < ActiveRecord::Base
|
149
|
+
as_enum :gender, Gender.find(:all).map { |g| [g.name.to_sym, g.id] } # map to array of symbols
|
150
|
+
end
|
151
|
+
|
152
|
+
Working with object backed values, the only requirement to enable this is that <em>a)</em> either a field name +name+ exists
|
153
|
+
or <em>b)</em> a custom method to convert an object to a symbolized form named +to_enum_sym+ (for general uses overriding
|
154
|
+
+to_enum+ is perfectly fine) exists:
|
155
|
+
|
156
|
+
class Status < ActiveRecord::Base
|
157
|
+
# this has a column named :name
|
158
|
+
STATUSES = self.find(:all, :order => :name)
|
159
|
+
end
|
160
|
+
|
161
|
+
class BankTransaction < ActiveRecord::Base
|
162
|
+
as_enum :status, Status.STATUSES
|
163
|
+
end
|
164
|
+
|
165
|
+
# what happens now? the id's of Status now serve as enumeration key and the
|
166
|
+
# Status object as the value so...
|
167
|
+
t = BankTransaction.new
|
168
|
+
t.pending!
|
169
|
+
t.status # => #<Status id: 1, name: "pending">
|
170
|
+
|
171
|
+
# and it's also possible to access the objects/values using:
|
172
|
+
BankTransaction.statuses(:pending) # => 1, access by symbol (not) the object!
|
173
|
+
BankTransaction.statuses.pending # => 1
|
174
|
+
BankTransaction.statuses.pending(true) # => #<Status id: 1, name: "pending">
|
175
|
+
|
176
|
+
== Known issues/Open items
|
177
|
+
|
178
|
+
* Maybe the <tt>:whiny</tt> option should default to <tt>false</tt>, so that generally no exceptions are thrown if a user fakes a request?
|
179
|
+
* Clean-up code and tests -> bring down LOC ;) (but maintain Code LOC vs. Test LOC ratio which is currently 1:2.9)
|
180
|
+
|
181
|
+
== Licence & Copyright
|
182
|
+
Copyright (c) 2009 by Lukas Westermann, Licenced under MIT Licence (see LICENCE file)
|
data/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the simple_enum plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the simple_enum plugin (results in doc/).'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'doc'
|
19
|
+
rdoc.title = 'SimpleEnum'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README.rdoc')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
rdoc.rdoc_files.include('LICENCE');
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'jeweler'
|
28
|
+
Jeweler::Tasks.new do |gemspec|
|
29
|
+
gemspec.name = "simple_enum"
|
30
|
+
gemspec.summary = "Simple enum-like field support for ActiveRecord (including validations and i18n)"
|
31
|
+
gemspec.email = "lukas.westermann@gmail.com"
|
32
|
+
gemspec.homepage = "http://github.com/lwe/simple_enum"
|
33
|
+
gemspec.authors = ["Lukas Westermann"]
|
34
|
+
end
|
35
|
+
Jeweler::GemcutterTasks.new
|
36
|
+
rescue LoadError
|
37
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
38
|
+
end
|
39
|
+
|
40
|
+
namespace :metrics do
|
41
|
+
desc 'Report code statistics for library and tests to shell.'
|
42
|
+
task :stats do |t|
|
43
|
+
require 'code_statistics'
|
44
|
+
dirs = {
|
45
|
+
'Libraries' => 'lib',
|
46
|
+
'Unit tests' => 'test'
|
47
|
+
}.map { |name,dir| [name, File.join(File.dirname(__FILE__), dir)] }
|
48
|
+
CodeStatistics.new(*dirs).to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'Report code coverage to HTML (doc/coverage) and shell (requires rcov).'
|
52
|
+
task :coverage do |t|
|
53
|
+
rm_f "doc/coverage"
|
54
|
+
mkdir_p "doc/coverage"
|
55
|
+
rcov = %(rcov -Ilib:test --exclude '\/gems\/' -o doc/coverage -T test/*_test.rb )
|
56
|
+
system rcov
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc 'Start IRB console with loaded test/test_helper.rb and sqlite db.'
|
61
|
+
task :console do |t|
|
62
|
+
chdir File.dirname(__FILE__)
|
63
|
+
exec 'irb -Ilib/ -r test/test_helper'
|
64
|
+
end
|
65
|
+
|
66
|
+
desc 'Clean up generated files.'
|
67
|
+
task :clean do |t|
|
68
|
+
FileUtils.rm_rf "doc"
|
69
|
+
FileUtils.rm_rf "pkg"
|
70
|
+
end
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'lib', 'simple_enum')
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SimpleEnum
|
2
|
+
module ArraySupport
|
3
|
+
|
4
|
+
# Magically convert an array to a hash, has some neat features
|
5
|
+
# for active record models and similar.
|
6
|
+
#
|
7
|
+
# TODO: add more documentation; allow block to be passed to customize key/value pairs
|
8
|
+
def to_hash_magic
|
9
|
+
v = enum_with_index.to_a unless first.is_a?(ActiveRecord::Base) or first.is_a?(Array)
|
10
|
+
v = map { |e| [e, e.id] } if first.is_a?(ActiveRecord::Base)
|
11
|
+
v ||= self
|
12
|
+
Hash[*v.flatten]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SimpleEnum
|
2
|
+
|
3
|
+
# Internal hash class, used to handle the enumerations et al.
|
4
|
+
# Works like to original +Hash+ class, but with some added value,
|
5
|
+
# like access to
|
6
|
+
#
|
7
|
+
#
|
8
|
+
class EnumHash < ::Hash
|
9
|
+
def initialize(hsh)
|
10
|
+
hsh = hsh.to_hash_magic unless hsh.is_a?(Hash)
|
11
|
+
|
12
|
+
@reverse_sym_lookup = {}
|
13
|
+
@sym_value_lookup = {}
|
14
|
+
|
15
|
+
hsh.each do |k,v|
|
16
|
+
sym = k.to_enum_sym
|
17
|
+
self[k] = v
|
18
|
+
@reverse_sym_lookup[sym] = k
|
19
|
+
@sym_value_lookup[sym] = v
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def default(k = nil)
|
24
|
+
@sym_value_lookup[k.to_enum_sym] if k
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(symbol, *args)
|
28
|
+
if @sym_value_lookup.has_key?(symbol.to_enum_sym)
|
29
|
+
return @reverse_sym_lookup[symbol.to_enum_sym] if args.first
|
30
|
+
self[symbol]
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SimpleEnum
|
2
|
+
module ObjectSupport
|
3
|
+
|
4
|
+
# Convert object to symbol for use in symbolized enum
|
5
|
+
# methods. Return value is supposed to be a symbol,
|
6
|
+
# though strings should work as well.
|
7
|
+
#
|
8
|
+
# The default behaviour is to try +to_sym+ first, then
|
9
|
+
# checks if a field named +name+ exists or finally falls
|
10
|
+
# back to +to_param+ method as provided by ActiveSupport.
|
11
|
+
#
|
12
|
+
# It's perfectly for subclasses to override this method,
|
13
|
+
# to provide custom +to_enum_sym+ behaviour, e.g. if
|
14
|
+
# the symbolized value is in e.g. +title+:
|
15
|
+
#
|
16
|
+
# class FormOfAddress < ActiveRecord::Base
|
17
|
+
# attr_accessor :title
|
18
|
+
# def to_enum_sym; title; end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# *Note*: to provide better looking methods values for +name+
|
22
|
+
# are <tt>parametereize('_')</tt>'d, so it might be a good idea to do
|
23
|
+
# the same thing in a custom +to_enum_sym+ method, like (for the
|
24
|
+
# example above):
|
25
|
+
#
|
26
|
+
# def to_enum_sym; title.parameterize('_').to_sym; end
|
27
|
+
#
|
28
|
+
# *TODO*: The current implementation does not handle nil values very
|
29
|
+
# gracefully, so if +name+ returns +nil+, it should be handled
|
30
|
+
# a bit better I suppose...
|
31
|
+
def to_enum_sym
|
32
|
+
return to_sym if respond_to?(:to_sym)
|
33
|
+
return name.to_s.parameterize('_').to_sym if respond_to?(:name)
|
34
|
+
to_param.to_sym unless blank? # fallback, unless empty...
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SimpleEnum
|
2
|
+
module Validation
|
3
|
+
|
4
|
+
# Validates an +as_enum+ field based on the value of it's column.
|
5
|
+
#
|
6
|
+
# Model:
|
7
|
+
# class User < ActiveRecord::Base
|
8
|
+
# as_enum :gender, [ :male, :female ]
|
9
|
+
# validates_as_enum :gender
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# View:
|
13
|
+
# <%= select(:user, :gender, User.genders.keys) %>
|
14
|
+
#
|
15
|
+
# Configuration options:
|
16
|
+
# * <tt>:message</tt> - A custom error message (default: is <tt>[:activerecord, :errors, :messages, :invalid_enum]</tt>).
|
17
|
+
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
18
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
19
|
+
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
20
|
+
# method, proc or string should return or evaluate to a true or false value.
|
21
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
22
|
+
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
23
|
+
# method, proc or string should return or evaluate to a true or false value.
|
24
|
+
def validates_as_enum(*attr_names)
|
25
|
+
configuration = { :on => :save }
|
26
|
+
configuration.update(attr_names.extract_options!)
|
27
|
+
attr_names.map! { |e| enum_definitions[e][:column] } # map to column name
|
28
|
+
|
29
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
30
|
+
enum_def = enum_definitions[attr_name]
|
31
|
+
unless send(enum_def[:name].to_s.pluralize).values.include?(value)
|
32
|
+
record.errors.add(enum_def[:name], :invalid_enum, :default => configuration[:message], :value => value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SimpleEnum
|
2
|
+
module VERSION #:nodoc:
|
3
|
+
def self.version
|
4
|
+
@VERSION_PARTS ||= YAML.load_file File.join(File.dirname(__FILE__), '..', '..', 'VERSION.yml')
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.to_s
|
8
|
+
@VERSION ||= [version[:major], version[:minor], version[:patch]].join('.').freeze
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
NAME = "simple_enum".freeze
|
13
|
+
ABOUT = "#{NAME} #{VERSION}".freeze
|
14
|
+
end
|
data/lib/simple_enum.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# SimpleEnum allows for cross-database, easy to use enum-like fields to be added to your
|
2
|
+
# ActiveRecord models. It does not rely on database specific column types like <tt>ENUM</tt> (MySQL),
|
3
|
+
# but instead on integer columns.
|
4
|
+
#
|
5
|
+
# Author:: Lukas Westermann
|
6
|
+
# Copyright:: Copyright (c) 2009 Lukas Westermann (Zurich, Switzerland)
|
7
|
+
# Licence:: MIT-Licence (http://www.opensource.org/licenses/mit-license.php)
|
8
|
+
#
|
9
|
+
# See the +as_enum+ documentation for more details.
|
10
|
+
|
11
|
+
require 'simple_enum/array_support'
|
12
|
+
require 'simple_enum/enum_hash'
|
13
|
+
require 'simple_enum/object_support'
|
14
|
+
require 'simple_enum/validation'
|
15
|
+
require 'simple_enum/version'
|
16
|
+
|
17
|
+
# Base module which gets included in <tt>ActiveRecord::Base</tt>. See documentation
|
18
|
+
# of +SimpleEnum::ClassMethods+ for more details.
|
19
|
+
module SimpleEnum
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
# Provides configurability to SimpleEnum, allows to override some defaults which are
|
24
|
+
# defined for all uses of +as_enum+. Most options from +as_enum+ are available, such as:
|
25
|
+
# * <tt>:prefix</tt> - Define a prefix, which is prefixed to the shortcut methods (e.g. <tt><symbol>!</tt> and
|
26
|
+
# <tt><symbol>?</tt>), if it's set to <tt>true</tt> the enumeration name is used as a prefix, else a custom
|
27
|
+
# prefix (symbol or string) (default is <tt>nil</tt> => no prefix)
|
28
|
+
# * <tt>:slim</tt> - If set to <tt>true</tt> no shortcut methods for all enumeration values are being generated, if
|
29
|
+
# set to <tt>:class</tt> only class-level shortcut methods are disabled (default is <tt>nil</tt> => they are generated)
|
30
|
+
# * <tt>:upcase</tt> - If set to +true+ the <tt>Klass.foos</tt> is named <tt>Klass.FOOS</tt>, why? To better suite some
|
31
|
+
# coding-styles (default is +false+ => downcase)
|
32
|
+
# * <tt>:whiny</tt> - Boolean value which if set to <tt>true</tt> will throw an <tt>ArgumentError</tt>
|
33
|
+
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
|
34
|
+
# <tt>false</tt> no exception is thrown and the internal value is set to <tt>nil</tt> (default is <tt>true</tt>)
|
35
|
+
def default_options
|
36
|
+
@default_options ||= {
|
37
|
+
:whiny => true,
|
38
|
+
:upcase => false
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def included(base) #:nodoc:
|
43
|
+
base.send :extend, ClassMethods
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
# Provides ability to create simple enumerations based on hashes or arrays, backed
|
49
|
+
# by integer columns (but not limited to integer columns).
|
50
|
+
#
|
51
|
+
# Columns are supposed to be suffixed by <tt>_cd</tt>, if not, use <tt>:column => 'the_column_name'</tt>,
|
52
|
+
# so some example migrations:
|
53
|
+
#
|
54
|
+
# add_column :users, :gender_cd, :integer
|
55
|
+
# add_column :users, :status, :integer # and a custom column...
|
56
|
+
#
|
57
|
+
# and then in your model:
|
58
|
+
#
|
59
|
+
# class User < ActiveRecord::Base
|
60
|
+
# as_enum :gender, [:male, :female]
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # or use a hash:
|
64
|
+
#
|
65
|
+
# class User < ActiveRecord::Base
|
66
|
+
# as_enum :status, { :active => 1, :inactive => 0, :archived => 2, :deleted => 3 }, :column => 'status'
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# Now it's possible to access the enumeration and the internally stored value like:
|
70
|
+
#
|
71
|
+
# john_doe = User.new
|
72
|
+
# john_doe.gender # => nil
|
73
|
+
# john_doe.gender = :male
|
74
|
+
# john_doe.gender # => :male
|
75
|
+
# john_doe.gender_cd # => 0
|
76
|
+
#
|
77
|
+
# And to make life a tad easier: a few shortcut methods to work with the enumeration are also created.
|
78
|
+
#
|
79
|
+
# john_doe.male? # => true
|
80
|
+
# john_doe.female? # => false
|
81
|
+
# john_doe.female! # => :female (set's gender to :female => gender_cd = 1)
|
82
|
+
# john_doe.male? # => false
|
83
|
+
#
|
84
|
+
# Sometimes it's required to access the db-backed values, like e.g. in a query:
|
85
|
+
#
|
86
|
+
# User.genders # => { :male => 0, :female => 1}, values hash
|
87
|
+
# User.genders(:male) # => 0, value access (via hash)
|
88
|
+
# User.female # => 1, direct access
|
89
|
+
# User.find :all, :conditions => { :gender_cd => User.female } # => [...], list with all women
|
90
|
+
#
|
91
|
+
# To access the key/value assocations in a helper like the select helper or similar use:
|
92
|
+
#
|
93
|
+
# <%= select(:user, :gender, User.genders.keys)
|
94
|
+
#
|
95
|
+
# The generated shortcut methods (like <tt>male?</tt> or <tt>female!</tt> etc.) can also be prefixed
|
96
|
+
# using the <tt>:prefix</tt> option. If the value is <tt>true</tt>, the shortcut methods are prefixed
|
97
|
+
# with the name of the enumeration.
|
98
|
+
#
|
99
|
+
# class User < ActiveRecord::Base
|
100
|
+
# as_enum :gender, [:male, :female], :prefix => true
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# jane_doe = User.new
|
104
|
+
# jane_doe.gender = :female # this is still as-is
|
105
|
+
# jane_doe.gender_cd # => 1, and so it this
|
106
|
+
#
|
107
|
+
# jane_doe.gender_female? # => true (instead of jane_doe.female?)
|
108
|
+
#
|
109
|
+
# It is also possible to supply a custom prefix.
|
110
|
+
#
|
111
|
+
# class Item < ActiveRecord::Base
|
112
|
+
# as_enum :status, [:inactive, :active, :deleted], :prefix => :state
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# item = Item.new(:status => :active)
|
116
|
+
# item.state_inactive? # => false
|
117
|
+
# Item.state_deleted # => 2
|
118
|
+
# Item.status(:deleted) # => 2, same as above...
|
119
|
+
#
|
120
|
+
# To disable the generation of the shortcut methods for all enumeration values, add <tt>:slim => true</tt> to
|
121
|
+
# the options.
|
122
|
+
#
|
123
|
+
# class Address < ActiveRecord::Base
|
124
|
+
# as_enum :canton, {:aargau => 'ag', ..., :wallis => 'vs', :zug => 'zg', :zurich => 'zh'}, :slim => true
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# home = Address.new(:canton => :zurich, :street => 'Bahnhofstrasse 1', ...)
|
128
|
+
# home.canton # => :zurich
|
129
|
+
# home.canton_cd # => 'zh'
|
130
|
+
# home.aargau! # throws NoMethodError: undefined method `aargau!'
|
131
|
+
# Address.aargau # throws NoMethodError: undefined method `aargau`
|
132
|
+
#
|
133
|
+
# This is especially useful if there are (too) many enumeration values, or these shortcut methods
|
134
|
+
# are not required.
|
135
|
+
#
|
136
|
+
# === Configuration options:
|
137
|
+
# * <tt>:column</tt> - Specifies a custom column name, instead of the default suffixed <tt>_cd</tt> column
|
138
|
+
# * <tt>:prefix</tt> - Define a prefix, which is prefixed to the shortcut methods (e.g. <tt><symbol>!</tt> and
|
139
|
+
# <tt><symbol>?</tt>), if it's set to <tt>true</tt> the enumeration name is used as a prefix, else a custom
|
140
|
+
# prefix (symbol or string) (default is <tt>nil</tt> => no prefix)
|
141
|
+
# * <tt>:slim</tt> - If set to <tt>true</tt> no shortcut methods for all enumeration values are being generated, if
|
142
|
+
# set to <tt>:class</tt> only class-level shortcut methods are disabled (default is <tt>nil</tt> => they are generated)
|
143
|
+
# * <tt>:upcase</tt> - If set to +true+ the <tt>Klass.foos</tt> is named <tt>Klass.FOOS</tt>, why? To better suite some
|
144
|
+
# coding-styles (default is +false+ => downcase)
|
145
|
+
# * <tt>:whiny</tt> - Boolean value which if set to <tt>true</tt> will throw an <tt>ArgumentError</tt>
|
146
|
+
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
|
147
|
+
# <tt>false</tt> no exception is thrown and the internal value is set to <tt>nil</tt> (default is <tt>true</tt>)
|
148
|
+
def as_enum(enum_cd, values, options = {})
|
149
|
+
options = SimpleEnum.default_options.merge({ :column => "#{enum_cd}_cd" }).merge(options)
|
150
|
+
options.assert_valid_keys(:column, :whiny, :prefix, :slim, :upcase)
|
151
|
+
|
152
|
+
# convert array to hash...
|
153
|
+
values = SimpleEnum::EnumHash.new(values)
|
154
|
+
values_inverted = values.invert
|
155
|
+
|
156
|
+
# store info away
|
157
|
+
write_inheritable_attribute(:enum_definitions, {}) if enum_definitions.nil?
|
158
|
+
enum_definitions[enum_cd] = enum_definitions[options[:column]] = { :name => enum_cd, :column => options[:column], :options => options }
|
159
|
+
|
160
|
+
# generate getter
|
161
|
+
define_method("#{enum_cd}") do
|
162
|
+
id = read_attribute options[:column]
|
163
|
+
values_inverted[id]
|
164
|
+
end
|
165
|
+
|
166
|
+
# generate setter
|
167
|
+
define_method("#{enum_cd}=") do |new_value|
|
168
|
+
v = new_value.nil? ? nil : values[new_value.to_sym]
|
169
|
+
raise(ArgumentError, "Invalid enumeration value: #{new_value}") if (options[:whiny] and v.nil? and !new_value.nil?)
|
170
|
+
write_attribute options[:column], v
|
171
|
+
end
|
172
|
+
|
173
|
+
# allow access to defined values hash, e.g. in a select helper or finder method.
|
174
|
+
self_name = enum_cd.to_s.pluralize
|
175
|
+
self_name.upcase! if options[:upcase]
|
176
|
+
class_variable_set :"@@SE_#{self_name.upcase}", values
|
177
|
+
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
|
178
|
+
def self.#{self_name}(sym = nil)
|
179
|
+
return class_variable_get(:@@SE_#{self_name.upcase}) if sym.nil?
|
180
|
+
class_variable_get(:@@SE_#{self_name.upcase})[sym]
|
181
|
+
end
|
182
|
+
EOM
|
183
|
+
|
184
|
+
# only create if :slim is not defined
|
185
|
+
if options[:slim] != true
|
186
|
+
# create both, boolean operations and *bang* operations for each
|
187
|
+
# enum "value"
|
188
|
+
prefix = options[:prefix] && "#{options[:prefix] == true ? enum_cd : options[:prefix]}_"
|
189
|
+
|
190
|
+
values.each do |k,code|
|
191
|
+
sym = k.to_enum_sym
|
192
|
+
|
193
|
+
define_method("#{prefix}#{sym}?") do
|
194
|
+
code == read_attribute(options[:column])
|
195
|
+
end
|
196
|
+
define_method("#{prefix}#{sym}!") do
|
197
|
+
write_attribute options[:column], code
|
198
|
+
sym
|
199
|
+
end
|
200
|
+
|
201
|
+
# allow class access to each value
|
202
|
+
unless options[:slim] === :class
|
203
|
+
metaclass.send(:define_method, "#{prefix}#{sym}", Proc.new { |*args| args.first ? k : code })
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
include Validation
|
210
|
+
|
211
|
+
protected
|
212
|
+
# Returns enum definitions as defined by each call to
|
213
|
+
# +as_enum+.
|
214
|
+
def enum_definitions
|
215
|
+
read_inheritable_attribute(:enum_definitions)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Tie stuff together and load translations if ActiveRecord is defined
|
221
|
+
if Object.const_defined?('ActiveRecord')
|
222
|
+
Object.send(:include, SimpleEnum::ObjectSupport)
|
223
|
+
Array.send(:include, SimpleEnum::ArraySupport)
|
224
|
+
|
225
|
+
ActiveRecord::Base.send(:include, SimpleEnum)
|
226
|
+
I18n.load_path << File.join(File.dirname(__FILE__), '..', 'locales', 'en.yml')
|
227
|
+
end
|