activerecord-string-enum 0.0.1

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