activerecord-string-enum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 119c5e3985161d99305ceaffd9626b89e3f53e9b
4
+ data.tar.gz: c1ea2773a67525dd178650aba25b3253886f139a
5
+ SHA512:
6
+ metadata.gz: cfe8875234b49015f76bd59272f2dc94400ebdeddd9c34f828017463ec842a8a781e5dbb367dba15a179decc10fd10f25d0b60e69c64071a8954ccd00df34dc1
7
+ data.tar.gz: 1d30978531fb961b3b6fd0fc6ee38dd7cefc1f99d21f57c49af114f7b567d4a0f4e929c2fd2fd840c1a92c34f112a5b185b5d31c9de8178457e74b232e5cbdbe
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activerecord-string-enum.gemspec
4
+ gemspec
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2015 Phusion V.O.F.
2
+ Copyright (c) 2004-2015 David Heinemeier Hansson
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # Activerecord::StringEnum
2
+
3
+ Make ActiveRecord 4's Enum store as strings instead of integers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'activerecord-string-enum'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install activerecord-string-enum
20
+
21
+ ## Usage
22
+
23
+ class Task < ActiveRecord::Base
24
+ extend ActiveRecord::StringEnum
25
+
26
+ str_enum :status, [:running, :finished]
27
+ end
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it ( https://github.com/phusion/activerecord-string-enum/fork )
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_record/string_enum/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activerecord-string-enum"
8
+ spec.version = ActiveRecord::StringEnum::VERSION
9
+ spec.authors = ["Tinco Andringa"]
10
+ spec.email = ["tinco@phusion.nl"]
11
+ spec.summary = %q{Make ActiveRecord 4's Enum store as strings instead of integers.}
12
+ spec.description = %q{Make ActiveRecord 4's Enum store as strings instead of integers.}
13
+ spec.homepage = "https://github.com/phusion/activerecord-string-enum"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "activesupport", "4.2.0"
24
+ spec.add_development_dependency "activerecord", "4.2.0"
25
+ spec.add_development_dependency "erubis"
26
+ spec.add_development_dependency "sqlite3"
27
+ spec.add_development_dependency "mocha"
28
+ spec.add_development_dependency "simplecov"
29
+ end
@@ -0,0 +1,179 @@
1
+ require "active_record/string_enum/version"
2
+ require 'active_record/type/value'
3
+ require 'active_support/core_ext/object/deep_dup'
4
+
5
+ module ActiveRecord
6
+ # Declare an enum attribute where the values map to integers in the database,
7
+ # but can be queried by name. Example:
8
+ #
9
+ # class Conversation < ActiveRecord::Base
10
+ # enum status: [ :active, :archived ]
11
+ # end
12
+ #
13
+ # # conversation.update! status: :active
14
+ # conversation.active!
15
+ # conversation.active? # => true
16
+ # conversation.status # => "active"
17
+ #
18
+ # # conversation.update! status: :archived
19
+ # conversation.archived!
20
+ # conversation.archived? # => true
21
+ # conversation.status # => "archived"
22
+ #
23
+ # # conversation.update! status: :archived
24
+ # conversation.status = "archived"
25
+ #
26
+ # # conversation.update! status: nil
27
+ # conversation.status = nil
28
+ # conversation.status.nil? # => true
29
+ # conversation.status # => nil
30
+ #
31
+ # Scopes based on the allowed values of the enum field will be provided
32
+ # as well. With the above example:
33
+ #
34
+ # Conversation.active
35
+ # Conversation.archived
36
+ #
37
+ # Of course, you can also query them directly if the scopes doesn't fit your
38
+ # needs:
39
+ #
40
+ # Conversation.where(status: [:active, :archived])
41
+ # Conversation.where.not(status: :active)
42
+ #
43
+ # You can set the default value from the database declaration, like:
44
+ #
45
+ # create_table :conversations do |t|
46
+ # t.column :status, :string, default: :active
47
+ # end
48
+ #
49
+ # In rare circumstances you might need to access the mapping directly.
50
+ # The mappings are exposed through a class method with the pluralized attribute
51
+ # name.
52
+ #
53
+ # Conversation.statuses[0] # => :active
54
+ # Conversation.statuses[1] # => :archived
55
+ #
56
+
57
+ module StringEnum
58
+ def self.extended(base) # :nodoc:
59
+ base.class_attribute(:defined_str_enums)
60
+ base.defined_str_enums = {}
61
+ end
62
+
63
+ def inherited(base) # :nodoc:
64
+ base.defined_str_enums = defined_str_enums.deep_dup
65
+ super
66
+ end
67
+
68
+ class EnumType < Type::Value
69
+ def initialize(name, mapping)
70
+ @name = name
71
+ @mapping = mapping
72
+ end
73
+
74
+ def cast(value)
75
+ return if value.blank?
76
+
77
+ if mapping.include?(value.to_s)
78
+ value
79
+ else
80
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
81
+ end
82
+ end
83
+
84
+ def deserialize(value)
85
+ return if value.nil?
86
+ cast(value)
87
+ end
88
+
89
+ def serialize(value)
90
+ value.to_s
91
+ end
92
+
93
+ protected
94
+
95
+ attr_reader :name, :mapping
96
+ end
97
+
98
+ def str_enum(definitions)
99
+ klass = self
100
+ definitions.each do |name, values|
101
+ # statuses = [ ]
102
+ enum_values = values.map(&:to_s)
103
+ name = name.to_sym
104
+
105
+ # def self.statuses statuses end
106
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
107
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { values }
108
+
109
+ detect_enum_conflict!(name, name)
110
+ detect_enum_conflict!(name, "#{name}=")
111
+
112
+ # TODO: in Rails 4.2.1 this will be legal:
113
+ # attribute name, EnumType.new(name, enum_values)
114
+ # instead of the next two lines:
115
+ type = EnumType.new(name, enum_values)
116
+ define_method("#{name}=") { |value| self[name] = type.cast(value) }
117
+
118
+ _enum_methods_module.module_eval do
119
+ enum_values.each do |value|
120
+ # def active?() status == :active end
121
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
122
+ define_method("#{value}?") { self[name] == value }
123
+
124
+ # def active!() update! status: :active end
125
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
126
+ define_method("#{value}!") { update! name => value }
127
+
128
+ # scope :active, -> { where status: :active }
129
+ klass.send(:detect_enum_conflict!, name, value, true)
130
+ klass.scope value, -> { klass.where name => value }
131
+ end
132
+ end
133
+ defined_str_enums[name.to_s] = enum_values
134
+ end
135
+ end
136
+
137
+ private
138
+ def _enum_methods_module
139
+ @_enum_methods_module ||= begin
140
+ mod = Module.new
141
+ include mod
142
+ mod
143
+ end
144
+ end
145
+
146
+ ENUM_CONFLICT_MESSAGE = \
147
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
148
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
149
+ "by %{source}."
150
+
151
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
152
+ if klass_method && dangerous_class_method?(method_name)
153
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
154
+ enum: enum_name,
155
+ klass: self.name,
156
+ type: 'class',
157
+ method: method_name,
158
+ source: 'Active Record'
159
+ }
160
+ elsif !klass_method && dangerous_attribute_method?(method_name)
161
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
162
+ enum: enum_name,
163
+ klass: self.name,
164
+ type: 'instance',
165
+ method: method_name,
166
+ source: 'Active Record'
167
+ }
168
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
169
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
170
+ enum: enum_name,
171
+ klass: self.name,
172
+ type: 'instance',
173
+ method: method_name,
174
+ source: 'another enum'
175
+ }
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module StringEnum
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,209 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'config'
5
+
6
+ require 'active_support/testing/autorun'
7
+ require 'stringio'
8
+
9
+ require 'active_record'
10
+ require 'cases/test_case'
11
+ require 'active_support/dependencies'
12
+ require 'active_support/logger'
13
+ require 'active_support/core_ext/string/strip'
14
+
15
+ require 'support/config'
16
+ require 'support/connection'
17
+
18
+ # TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
19
+
20
+ Thread.abort_on_exception = true
21
+
22
+ # Show backtraces for deprecated behavior for quicker cleanup.
23
+ ActiveSupport::Deprecation.debug = true
24
+
25
+ # Disable available locale checks to avoid warnings running the test suite.
26
+ I18n.enforce_available_locales = false
27
+
28
+ # Enable raise errors in after_commit and after_rollback.
29
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
30
+
31
+ # Connect to the database
32
+ ARTest.connect
33
+
34
+ # Quote "type" if it's a reserved word for the current connection.
35
+ QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
36
+
37
+ def current_adapter?(*types)
38
+ types.any? do |type|
39
+ ActiveRecord::ConnectionAdapters.const_defined?(type) &&
40
+ ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters.const_get(type))
41
+ end
42
+ end
43
+
44
+ def in_memory_db?
45
+ current_adapter?(:SQLite3Adapter) &&
46
+ ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
47
+ end
48
+
49
+ def mysql_56?
50
+ current_adapter?(:Mysql2Adapter) &&
51
+ ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0"
52
+ end
53
+
54
+ def mysql_enforcing_gtid_consistency?
55
+ current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency')
56
+ end
57
+
58
+ def supports_savepoints?
59
+ ActiveRecord::Base.connection.supports_savepoints?
60
+ end
61
+
62
+ def with_env_tz(new_tz = 'US/Eastern')
63
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
64
+ yield
65
+ ensure
66
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
67
+ end
68
+
69
+ def with_timezone_config(cfg)
70
+ verify_default_timezone_config
71
+
72
+ old_default_zone = ActiveRecord::Base.default_timezone
73
+ old_awareness = ActiveRecord::Base.time_zone_aware_attributes
74
+ old_zone = Time.zone
75
+
76
+ if cfg.has_key?(:default)
77
+ ActiveRecord::Base.default_timezone = cfg[:default]
78
+ end
79
+ if cfg.has_key?(:aware_attributes)
80
+ ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
81
+ end
82
+ if cfg.has_key?(:zone)
83
+ Time.zone = cfg[:zone]
84
+ end
85
+ yield
86
+ ensure
87
+ ActiveRecord::Base.default_timezone = old_default_zone
88
+ ActiveRecord::Base.time_zone_aware_attributes = old_awareness
89
+ Time.zone = old_zone
90
+ end
91
+
92
+ # This method makes sure that tests don't leak global state related to time zones.
93
+ EXPECTED_ZONE = nil
94
+ EXPECTED_DEFAULT_TIMEZONE = :utc
95
+ EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
96
+ def verify_default_timezone_config
97
+ if Time.zone != EXPECTED_ZONE
98
+ $stderr.puts <<-MSG
99
+ \n#{self}
100
+ Global state `Time.zone` was leaked.
101
+ Expected: #{EXPECTED_ZONE}
102
+ Got: #{Time.zone}
103
+ MSG
104
+ end
105
+ if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
106
+ $stderr.puts <<-MSG
107
+ \n#{self}
108
+ Global state `ActiveRecord::Base.default_timezone` was leaked.
109
+ Expected: #{EXPECTED_DEFAULT_TIMEZONE}
110
+ Got: #{ActiveRecord::Base.default_timezone}
111
+ MSG
112
+ end
113
+ if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
114
+ $stderr.puts <<-MSG
115
+ \n#{self}
116
+ Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
117
+ Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
118
+ Got: #{ActiveRecord::Base.time_zone_aware_attributes}
119
+ MSG
120
+ end
121
+ end
122
+
123
+ def enable_extension!(extension, connection)
124
+ return false unless connection.supports_extensions?
125
+ return connection.reconnect! if connection.extension_enabled?(extension)
126
+
127
+ connection.enable_extension extension
128
+ connection.commit_db_transaction
129
+ connection.reconnect!
130
+ end
131
+
132
+ def disable_extension!(extension, connection)
133
+ return false unless connection.supports_extensions?
134
+ return true unless connection.extension_enabled?(extension)
135
+
136
+ connection.disable_extension extension
137
+ connection.reconnect!
138
+ end
139
+
140
+ class ActiveSupport::TestCase
141
+ include ActiveRecord::TestFixtures
142
+
143
+ self.fixture_path = FIXTURES_ROOT
144
+ self.use_instantiated_fixtures = false
145
+ self.use_transactional_fixtures = true
146
+
147
+ def create_fixtures(*fixture_set_names, &block)
148
+ ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block)
149
+ end
150
+ end
151
+
152
+ def load_schema
153
+ # silence verbose schema loading
154
+ original_stdout = $stdout
155
+ $stdout = StringIO.new
156
+
157
+ adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
158
+ adapter_specific_schema_file = SCHEMA_ROOT + "/#{adapter_name}_specific_schema.rb"
159
+
160
+ load SCHEMA_ROOT + "/schema.rb"
161
+
162
+ if File.exist?(adapter_specific_schema_file)
163
+ load adapter_specific_schema_file
164
+ end
165
+ ensure
166
+ $stdout = original_stdout
167
+ end
168
+
169
+ load_schema
170
+
171
+ class SQLSubscriber
172
+ attr_reader :logged
173
+ attr_reader :payloads
174
+
175
+ def initialize
176
+ @logged = []
177
+ @payloads = []
178
+ end
179
+
180
+ def start(name, id, payload)
181
+ @payloads << payload
182
+ @logged << [payload[:sql].squish, payload[:name], payload[:binds]]
183
+ end
184
+
185
+ def finish(name, id, payload); end
186
+ end
187
+
188
+ module InTimeZone
189
+ private
190
+
191
+ def in_time_zone(zone)
192
+ old_zone = Time.zone
193
+ old_tz = ActiveRecord::Base.time_zone_aware_attributes
194
+
195
+ Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
196
+ ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
197
+ yield
198
+ ensure
199
+ Time.zone = old_zone
200
+ ActiveRecord::Base.time_zone_aware_attributes = old_tz
201
+ end
202
+ end
203
+
204
+ require 'mocha/setup' # FIXME: stop using mocha
205
+
206
+ # FIXME: we have tests that depend on run order, we should fix that and
207
+ # remove this method call.
208
+ require 'active_support/test_case'
209
+ ActiveSupport::TestCase.test_order = :sorted
@@ -0,0 +1,295 @@
1
+ require 'active_record/string_enum'
2
+ require 'cases/helper'
3
+ require 'models/book'
4
+
5
+ class EnumTest < ActiveRecord::TestCase
6
+ fixtures :books
7
+
8
+ setup do
9
+ @book = books(:awdr)
10
+ end
11
+
12
+ test "query state by predicate" do
13
+ assert @book.proposed?
14
+ assert_not @book.written?
15
+ assert_not @book.published?
16
+
17
+ assert @book.unread?
18
+ end
19
+
20
+ test "query state with strings" do
21
+ assert_equal "proposed", @book.status
22
+ assert_equal "unread", @book.read_status
23
+ end
24
+
25
+ test "find via scope" do
26
+ assert_equal @book, Book.proposed.first
27
+ assert_equal @book, Book.unread.first
28
+ end
29
+
30
+ test "update by declaration" do
31
+ @book.written!
32
+ assert @book.written?
33
+ end
34
+
35
+ test "update by setter" do
36
+ @book.update! status: :written
37
+ assert @book.written?
38
+ end
39
+
40
+ test "enum methods are overwritable" do
41
+ assert_equal "do publish work...", @book.published!
42
+ assert @book.published?
43
+ end
44
+
45
+ test "direct assignment" do
46
+ @book.status = :written
47
+ assert @book.written?
48
+ end
49
+
50
+ test "assign string value" do
51
+ @book.status = "written"
52
+ assert @book.written?
53
+ end
54
+
55
+ test "enum changed attributes" do
56
+ old_status = @book.status
57
+ @book.status = :published
58
+ assert_equal old_status, @book.changed_attributes[:status]
59
+ end
60
+
61
+ test "enum changes" do
62
+ old_status = @book.status
63
+ @book.status = :published
64
+ assert_equal [old_status, 'published'], @book.changes[:status]
65
+ end
66
+
67
+ test "enum attribute was" do
68
+ old_status = @book.status
69
+ @book.status = :published
70
+ assert_equal old_status, @book.attribute_was(:status)
71
+ end
72
+
73
+ test "enum attribute changed" do
74
+ @book.status = :published
75
+ assert @book.attribute_changed?(:status)
76
+ end
77
+
78
+ test "enum attribute changed to" do
79
+ @book.status = :published
80
+ assert @book.attribute_changed?(:status, to: 'published')
81
+ end
82
+
83
+ test "enum attribute changed from" do
84
+ old_status = @book.status
85
+ @book.status = :published
86
+ assert @book.attribute_changed?(:status, from: old_status)
87
+ end
88
+
89
+ test "enum attribute changed from old status to new status" do
90
+ old_status = @book.status
91
+ @book.status = :published
92
+ assert @book.attribute_changed?(:status, from: old_status, to: 'published')
93
+ end
94
+
95
+ test "enum didn't change" do
96
+ old_status = @book.status
97
+ @book.status = old_status
98
+ assert_not @book.attribute_changed?(:status)
99
+ end
100
+
101
+ test "persist changes that are dirty" do
102
+ @book.status = :published
103
+ assert @book.attribute_changed?(:status)
104
+ @book.status = :written
105
+ assert @book.attribute_changed?(:status)
106
+ end
107
+
108
+ test "reverted changes that are not dirty" do
109
+ old_status = @book.status
110
+ @book.status = :published
111
+ assert @book.attribute_changed?(:status)
112
+ @book.status = old_status
113
+ assert_not @book.attribute_changed?(:status)
114
+ end
115
+
116
+ test "reverted changes are not dirty going from nil to value and back" do
117
+ book = Book.create!(nullable_status: nil)
118
+
119
+ book.nullable_status = :married
120
+ assert book.attribute_changed?(:nullable_status)
121
+
122
+ book.nullable_status = nil
123
+ assert_not book.attribute_changed?(:nullable_status)
124
+ end
125
+
126
+ test "assign non existing value raises an error" do
127
+ e = assert_raises(ArgumentError) do
128
+ @book.status = :unknown
129
+ end
130
+ assert_equal "'unknown' is not a valid status", e.message
131
+ end
132
+
133
+ test "assign nil value" do
134
+ @book.status = nil
135
+ assert @book.status.nil?
136
+ end
137
+
138
+ test "assign empty string value" do
139
+ @book.status = ''
140
+ assert @book.status.nil?, "Book was blank?: #{@book.status.blank?}"
141
+ end
142
+
143
+ test "assign long empty string value" do
144
+ @book.status = ' '
145
+ assert @book.status.nil?
146
+ end
147
+
148
+ test "constant to access the mapping" do
149
+ assert_equal Book.statuses[0], :proposed
150
+ assert_equal Book.statuses[1], :written
151
+ assert_equal Book.statuses[2], :published
152
+ end
153
+
154
+ test "building new objects with enum scopes" do
155
+ assert Book.written.build.written?
156
+ assert Book.read.build.read?
157
+ end
158
+
159
+ test "creating new objects with enum scopes" do
160
+ assert Book.written.create.written?
161
+ assert Book.read.create.read?
162
+ end
163
+
164
+ test "_before_type_cast returns the enum label (required for form fields)" do
165
+ assert_equal "proposed", @book.status_before_type_cast
166
+ end
167
+
168
+ test "reserved enum names" do
169
+ klass = Class.new(ActiveRecord::Base) do
170
+ self.table_name = "books"
171
+ enum status: [:proposed, :written, :published]
172
+ end
173
+
174
+ conflicts = [
175
+ :column, # generates class method .columns, which conflicts with an AR method
176
+ :logger, # generates #logger, which conflicts with an AR method
177
+ :attributes, # generates #attributes=, which conflicts with an AR method
178
+ ]
179
+
180
+ conflicts.each_with_index do |name, i|
181
+ assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do
182
+ klass.class_eval { enum name => ["value_#{i}"] }
183
+ end
184
+ end
185
+ end
186
+
187
+ test "reserved enum values" do
188
+ klass = Class.new(ActiveRecord::Base) do
189
+ self.table_name = "books"
190
+ enum status: [:proposed, :written, :published]
191
+ end
192
+
193
+ conflicts = [
194
+ :new, # generates a scope that conflicts with an AR class method
195
+ :valid, # generates #valid?, which conflicts with an AR method
196
+ :save, # generates #save!, which conflicts with an AR method
197
+ :proposed, # same value as an existing enum
198
+ :public, :private, :protected, # some important methods on Module and Class
199
+ :name, :parent, :superclass
200
+ ]
201
+
202
+ conflicts.each_with_index do |value, i|
203
+ assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
204
+ klass.class_eval { enum "status_#{i}" => [value] }
205
+ end
206
+ end
207
+ end
208
+
209
+ test "overriding enum method should not raise" do
210
+ assert_nothing_raised do
211
+ Class.new(ActiveRecord::Base) do
212
+ self.table_name = "books"
213
+
214
+ def published!
215
+ super
216
+ "do publish work..."
217
+ end
218
+
219
+ enum status: [:proposed, :written, :published]
220
+
221
+ def written!
222
+ super
223
+ "do written work..."
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ test "validate uniqueness" do
230
+ klass = Class.new(ActiveRecord::Base) do
231
+ extend ActiveRecord::StringEnum
232
+ def self.name; 'Book'; end
233
+ str_enum status: [:proposed, :written]
234
+ validates_uniqueness_of :status
235
+ end
236
+ klass.delete_all
237
+ klass.create!(status: "proposed")
238
+ book = klass.new(status: "written")
239
+ assert book.valid?
240
+ book.status = "proposed"
241
+ assert_not book.valid?
242
+ end
243
+
244
+ test "validate inclusion of value in array" do
245
+ klass = Class.new(ActiveRecord::Base) do
246
+ extend ActiveRecord::StringEnum
247
+ def self.name; 'Book'; end
248
+ str_enum status: [:proposed, :written]
249
+ validates_inclusion_of :status, in: ["written"]
250
+ end
251
+ klass.delete_all
252
+ invalid_book = klass.new(status: "proposed")
253
+ assert_not invalid_book.valid?
254
+ valid_book = klass.new(status: "written")
255
+ assert valid_book.valid?
256
+ end
257
+
258
+ test "enums are distinct per class" do
259
+ klass1 = Class.new(ActiveRecord::Base) do
260
+ extend ActiveRecord::StringEnum
261
+ self.table_name = "books"
262
+ str_enum status: [:proposed, :written]
263
+ end
264
+
265
+ klass2 = Class.new(ActiveRecord::Base) do
266
+ extend ActiveRecord::StringEnum
267
+ self.table_name = "books"
268
+ str_enum status: [:drafted, :uploaded]
269
+ end
270
+
271
+ book1 = klass1.proposed.create!
272
+ book1.status = :written
273
+ assert_equal ['proposed', 'written'], book1.status_change
274
+
275
+ book2 = klass2.drafted.create!
276
+ book2.status = :uploaded
277
+ assert_equal ['drafted', 'uploaded'], book2.status_change
278
+ end
279
+
280
+ test "enums are inheritable" do
281
+ subklass1 = Class.new(Book)
282
+
283
+ subklass2 = Class.new(Book) do
284
+ str_enum status: [:drafted, :uploaded]
285
+ end
286
+
287
+ book1 = subklass1.proposed.create!
288
+ book1.status = :written
289
+ assert_equal ['proposed', 'written'], book1.status_change
290
+
291
+ book2 = subklass2.drafted.create!
292
+ book2.status = :uploaded
293
+ assert_equal ['drafted', 'uploaded'], book2.status_change
294
+ end
295
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/test_case'
2
+
3
+ module ActiveRecord
4
+ # = Active Record Test Case
5
+ #
6
+ # Defines some test assertions to test against SQL queries.
7
+ class TestCase < ActiveSupport::TestCase #:nodoc:
8
+ def teardown
9
+ SQLCounter.clear_log
10
+ end
11
+ end
12
+
13
+ class SQLCounter
14
+ class << self
15
+ attr_accessor :ignored_sql, :log, :log_all
16
+ def clear_log; self.log = []; self.log_all = []; end
17
+ end
18
+
19
+ self.clear_log
20
+
21
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
22
+
23
+ # FIXME: this needs to be refactored so specific database can add their own
24
+ # ignored SQL, or better yet, use a different notification for the queries
25
+ # instead examining the SQL content.
26
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
27
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
28
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
29
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
30
+
31
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
32
+ ignored_sql.concat db_ignored_sql
33
+ end
34
+
35
+ attr_reader :ignore
36
+
37
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
38
+ @ignore = ignore
39
+ end
40
+
41
+ def call(name, start, finish, message_id, values)
42
+ sql = values[:sql]
43
+
44
+ # FIXME: this seems bad. we should probably have a better way to indicate
45
+ # the query was cached
46
+ return if 'CACHE' == values[:name]
47
+
48
+ self.class.log_all << sql
49
+ self.class.log << sql unless ignore =~ sql
50
+ end
51
+ end
52
+
53
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
54
+ end
@@ -0,0 +1,5 @@
1
+ TEST_ROOT = File.expand_path(File.dirname(__FILE__))
2
+ ASSETS_ROOT = TEST_ROOT + "/assets"
3
+ FIXTURES_ROOT = TEST_ROOT + "/fixtures"
4
+ MIGRATIONS_ROOT = TEST_ROOT + "/migrations"
5
+ SCHEMA_ROOT = TEST_ROOT + "/schema"
@@ -0,0 +1,12 @@
1
+ default_connection: sqlite3_mem
2
+
3
+ with_manual_interventions: false
4
+
5
+ connections:
6
+ sqlite3_mem:
7
+ arunit:
8
+ adapter: sqlite3
9
+ database: ':memory:'
10
+ arunit2:
11
+ adapter: sqlite3
12
+ database: ':memory:'
@@ -0,0 +1,5 @@
1
+ awdr:
2
+ id: 1
3
+
4
+ rfr:
5
+ id: 2
@@ -0,0 +1,3 @@
1
+ class ARUnit2Model < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,12 @@
1
+ class Book < ActiveRecord::Base
2
+ extend ActiveRecord::StringEnum
3
+
4
+ str_enum status: [:proposed, :written, :published]
5
+ str_enum read_status: [:unread, :reading, :read]
6
+ str_enum nullable_status: [:single, :married]
7
+
8
+ def published!
9
+ super
10
+ "do publish work..."
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :books, force: true do |t|
3
+ t.column :status, :string, default: 'proposed'
4
+ t.column :read_status, :string, default: 'unread'
5
+ t.column :nullable_status, :string
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ require 'yaml'
2
+ require 'erubis'
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module ARTest
7
+ class << self
8
+ def config
9
+ @config ||= read_config
10
+ end
11
+
12
+ private
13
+
14
+ def config_file
15
+ Pathname.new(ENV['ARCONFIG'] || TEST_ROOT + '/config.yml')
16
+ end
17
+
18
+ def read_config
19
+ expand_config(YAML.load_file(config_file))
20
+ end
21
+
22
+ def expand_config(config)
23
+ config['connections'].each do |adapter, connection|
24
+ dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']]
25
+ dbs.each do |name, dbname|
26
+ connection[name]['database'] ||= dbname
27
+ connection[name]['adapter'] ||= adapter
28
+ end
29
+ end
30
+
31
+ config
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ require 'models/arunit2_model'
2
+ require 'active_support/logger'
3
+
4
+ module ARTest
5
+ def self.connection_name
6
+ ENV['ARCONN'] || config['default_connection']
7
+ end
8
+
9
+ def self.connection_config
10
+ config['connections'][connection_name]
11
+ end
12
+
13
+ def self.connect
14
+ puts "Using #{connection_name}"
15
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
16
+ ActiveRecord::Base.configurations = connection_config
17
+ ActiveRecord::Base.establish_connection :arunit
18
+ ARUnit2Model.establish_connection :arunit2
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module ConnectionHelper
2
+ def run_without_connection
3
+ original_connection = ActiveRecord::Base.remove_connection
4
+ yield original_connection
5
+ ensure
6
+ ActiveRecord::Base.establish_connection(original_connection)
7
+ end
8
+
9
+ # Used to drop all cache query plans in tests.
10
+ def reset_connection
11
+ original_connection = ActiveRecord::Base.remove_connection
12
+ ActiveRecord::Base.establish_connection(original_connection)
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module DdlHelper
2
+ def with_example_table(connection, table_name, definition = nil)
3
+ connection.execute("CREATE TABLE #{table_name}(#{definition})")
4
+ yield
5
+ ensure
6
+ connection.execute("DROP TABLE #{table_name}")
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module SchemaDumpingHelper
2
+ def dump_table_schema(table, connection = ActiveRecord::Base.connection)
3
+ old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
4
+ ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
5
+ stream = StringIO.new
6
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
7
+ stream.string
8
+ ensure
9
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
10
+ end
11
+
12
+ def dump_all_table_schema(ignore_tables)
13
+ old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, ignore_tables
14
+ stream = StringIO.new
15
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
16
+ stream.string
17
+ ensure
18
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,193 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-string-enum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tinco Andringa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 4.2.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 4.2.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: erubis
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mocha
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Make ActiveRecord 4's Enum store as strings instead of integers.
126
+ email:
127
+ - tinco@phusion.nl
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - Gemfile
134
+ - LICENSE.txt
135
+ - README.md
136
+ - Rakefile
137
+ - activerecord-string-enum.gemspec
138
+ - lib/active_record/string_enum.rb
139
+ - lib/active_record/string_enum/version.rb
140
+ - test/cases/helper.rb
141
+ - test/cases/str_enum_test.rb
142
+ - test/cases/test_case.rb
143
+ - test/config.rb
144
+ - test/config.yml
145
+ - test/fixtures/books.yml
146
+ - test/models/arunit2_model.rb
147
+ - test/models/book.rb
148
+ - test/schema/schema.rb
149
+ - test/support/config.rb
150
+ - test/support/connection.rb
151
+ - test/support/connection_helper.rb
152
+ - test/support/ddl_helper.rb
153
+ - test/support/schema_dumping_helper.rb
154
+ homepage: https://github.com/phusion/activerecord-string-enum
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.2.2
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Make ActiveRecord 4's Enum store as strings instead of integers.
178
+ test_files:
179
+ - test/cases/helper.rb
180
+ - test/cases/str_enum_test.rb
181
+ - test/cases/test_case.rb
182
+ - test/config.rb
183
+ - test/config.yml
184
+ - test/fixtures/books.yml
185
+ - test/models/arunit2_model.rb
186
+ - test/models/book.rb
187
+ - test/schema/schema.rb
188
+ - test/support/config.rb
189
+ - test/support/connection.rb
190
+ - test/support/connection_helper.rb
191
+ - test/support/ddl_helper.rb
192
+ - test/support/schema_dumping_helper.rb
193
+ has_rdoc: