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 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.