simple_enum 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|