simple_enum 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ doc
2
+ pkg
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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 3
4
+ :patch: 0
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
@@ -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
data/locales/en.yml ADDED
@@ -0,0 +1,6 @@
1
+ # english translation
2
+ en:
3
+ activerecord:
4
+ errors:
5
+ messages:
6
+ invalid_enum: invalid option supplied.