Skip to content

Commit 7c65a4b

Browse files
Make enums validatable without raising error (#49100)
* Make enums validatable without raising error * Trigger fail CI Co-authored-by: Rafael Mendonça França <rafael@rubyonrails.org>
1 parent cdbc9b7 commit 7c65a4b

File tree

3 files changed

+114
-3
lines changed

3 files changed

+114
-3
lines changed

activerecord/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@
99

1010
*Nikita Vasilevsky*
1111

12+
* Add validation option for `enum`
13+
14+
```ruby
15+
class Contract < ApplicationRecord
16+
enum :status, %w[in_progress completed], validate: true
17+
end
18+
Contract.new(status: "unknown").valid? # => false
19+
Contract.new(status: nil).valid? # => false
20+
Contract.new(status: "completed").valid? # => true
21+
22+
class Contract < ApplicationRecord
23+
enum :status, %w[in_progress completed], validate: { allow_nil: true }
24+
end
25+
Contract.new(status: "unknown").valid? # => false
26+
Contract.new(status: nil).valid? # => true
27+
Contract.new(status: "completed").valid? # => true
28+
```
29+
30+
*Edem Topuzov*
31+
1232
* Allow batching methods to use already loaded relation if available
1333

1434
Calling batch methods on already loaded relations will use the records previously loaded instead of retrieving

activerecord/lib/active_record/enum.rb

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,50 @@ module ActiveRecord
118118
# class Conversation < ActiveRecord::Base
119119
# enum :status, [ :active, :archived ], instance_methods: false
120120
# end
121+
#
122+
# If you want the enum value to be validated before saving, use the option +:validate+:
123+
#
124+
# class Conversation < ActiveRecord::Base
125+
# enum :status, [ :active, :archived ], validate: true
126+
# end
127+
#
128+
# conversation = Conversation.new
129+
#
130+
# conversation.status = :unknown
131+
# conversation.valid? # => false
132+
#
133+
# conversation.status = nil
134+
# conversation.valid? # => false
135+
#
136+
# conversation.status = :active
137+
# conversation.valid? # => true
138+
#
139+
# It is also possible to pass additional validation options:
140+
#
141+
# class Conversation < ActiveRecord::Base
142+
# enum :status, [ :active, :archived ], validate: { allow_nil: true }
143+
# end
144+
#
145+
# conversation = Conversation.new
146+
#
147+
# conversation.status = :unknown
148+
# conversation.valid? # => false
149+
#
150+
# conversation.status = nil
151+
# conversation.valid? # => true
152+
#
153+
# conversation.status = :active
154+
# conversation.valid? # => true
155+
#
156+
# Otherwise +ArgumentError+ will raise:
157+
#
158+
# class Conversation < ActiveRecord::Base
159+
# enum :status, [ :active, :archived ]
160+
# end
161+
#
162+
# conversation = Conversation.new
163+
#
164+
# conversation.status = :unknown # 'unknown' is not a valid status (ArgumentError)
121165
module Enum
122166
def self.extended(base) # :nodoc:
123167
base.class_attribute(:defined_enums, instance_writer: false, default: {})
@@ -135,10 +179,11 @@ def load_schema! # :nodoc:
135179
class EnumType < Type::Value # :nodoc:
136180
delegate :type, to: :subtype
137181

138-
def initialize(name, mapping, subtype)
182+
def initialize(name, mapping, subtype, raise_on_invalid_values: true)
139183
@name = name
140184
@mapping = mapping
141185
@subtype = subtype
186+
@_raise_on_invalid_values = raise_on_invalid_values
142187
end
143188

144189
def cast(value)
@@ -164,6 +209,8 @@ def serializable?(value, &block)
164209
end
165210

166211
def assert_valid_value(value)
212+
return unless @_raise_on_invalid_values
213+
167214
unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
168215
raise ArgumentError, "'#{value}' is not a valid #{name}"
169216
end
@@ -193,7 +240,7 @@ def inherited(base)
193240
super
194241
end
195242

196-
def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, **options)
243+
def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
197244
assert_valid_enum_definition_values(values)
198245
# statuses = { }
199246
enum_values = ActiveSupport::HashWithIndifferentAccess.new
@@ -209,7 +256,7 @@ def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods
209256

210257
attribute(name, **options) do |subtype|
211258
subtype = subtype.subtype if EnumType === subtype
212-
EnumType.new(name, enum_values, subtype)
259+
EnumType.new(name, enum_values, subtype, raise_on_invalid_values: !validate)
213260
end
214261

215262
value_method_names = []
@@ -241,6 +288,12 @@ def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods
241288
end
242289
end
243290
detect_negative_enum_conditions!(value_method_names) if scopes
291+
292+
if validate
293+
validate = {} unless Hash === validate
294+
validates_inclusion_of name, in: enum_values.keys, **validate
295+
end
296+
244297
enum_values.freeze
245298
end
246299

activerecord/test/cases/enum_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,44 @@ class EnumTest < ActiveRecord::TestCase
313313
assert_equal "'unknown' is not a valid status", e.message
314314
end
315315

316+
test "validation with 'validate: true' option" do
317+
klass = Class.new(ActiveRecord::Base) do
318+
def self.name; "Book"; end
319+
enum :status, [:proposed, :written], validate: true
320+
end
321+
322+
valid_book = klass.new(status: "proposed")
323+
assert_predicate valid_book, :valid?
324+
325+
valid_book = klass.new(status: "written")
326+
assert_predicate valid_book, :valid?
327+
328+
invalid_book = klass.new(status: nil)
329+
assert_not_predicate invalid_book, :valid?
330+
331+
invalid_book = klass.new(status: "unknown")
332+
assert_not_predicate invalid_book, :valid?
333+
end
334+
335+
test "validation with 'validate: hash' option" do
336+
klass = Class.new(ActiveRecord::Base) do
337+
def self.name; "Book"; end
338+
enum :status, [:proposed, :written], validate: { allow_nil: true }
339+
end
340+
341+
valid_book = klass.new(status: "proposed")
342+
assert_predicate valid_book, :valid?
343+
344+
valid_book = klass.new(status: "written")
345+
assert_predicate valid_book, :valid?
346+
347+
valid_book = klass.new(status: nil)
348+
assert_predicate valid_book, :valid?
349+
350+
invalid_book = klass.new(status: "unknown")
351+
assert_not_predicate invalid_book, :valid?
352+
end
353+
316354
test "NULL values from database should be casted to nil" do
317355
Book.where(id: @book.id).update_all("status = NULL")
318356
assert_nil @book.reload.status

0 commit comments

Comments
 (0)