activemodel 6.1.4.1 → 7.0.0.rc2

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -66
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -3
  5. data/lib/active_model/api.rb +99 -0
  6. data/lib/active_model/attribute.rb +4 -0
  7. data/lib/active_model/attribute_methods.rb +65 -81
  8. data/lib/active_model/attribute_set/builder.rb +1 -10
  9. data/lib/active_model/attribute_set.rb +4 -1
  10. data/lib/active_model/attributes.rb +15 -12
  11. data/lib/active_model/callbacks.rb +1 -1
  12. data/lib/active_model/conversion.rb +2 -2
  13. data/lib/active_model/dirty.rb +6 -5
  14. data/lib/active_model/errors.rb +35 -235
  15. data/lib/active_model/gem_version.rb +4 -4
  16. data/lib/active_model/locale/en.yml +1 -0
  17. data/lib/active_model/model.rb +6 -59
  18. data/lib/active_model/naming.rb +15 -8
  19. data/lib/active_model/secure_password.rb +2 -1
  20. data/lib/active_model/serialization.rb +7 -2
  21. data/lib/active_model/translation.rb +1 -1
  22. data/lib/active_model/type/date.rb +1 -1
  23. data/lib/active_model/type/helpers/numeric.rb +9 -1
  24. data/lib/active_model/type/helpers/time_value.rb +3 -3
  25. data/lib/active_model/type/integer.rb +4 -1
  26. data/lib/active_model/type/registry.rb +9 -41
  27. data/lib/active_model/type/time.rb +1 -1
  28. data/lib/active_model/type.rb +6 -5
  29. data/lib/active_model/validations/absence.rb +1 -1
  30. data/lib/active_model/validations/clusivity.rb +1 -1
  31. data/lib/active_model/validations/comparability.rb +29 -0
  32. data/lib/active_model/validations/comparison.rb +82 -0
  33. data/lib/active_model/validations/confirmation.rb +4 -4
  34. data/lib/active_model/validations/numericality.rb +28 -21
  35. data/lib/active_model/validations.rb +4 -4
  36. data/lib/active_model/validator.rb +2 -2
  37. data/lib/active_model.rb +2 -1
  38. metadata +14 -10
@@ -4,25 +4,26 @@ require "active_model/attribute_set"
4
4
  require "active_model/attribute/user_provided_default"
5
5
 
6
6
  module ActiveModel
7
- module Attributes #:nodoc:
7
+ module Attributes # :nodoc:
8
8
  extend ActiveSupport::Concern
9
9
  include ActiveModel::AttributeMethods
10
10
 
11
11
  included do
12
- attribute_method_suffix "="
12
+ attribute_method_suffix "=", parameters: "value"
13
13
  class_attribute :attribute_types, :_default_attributes, instance_accessor: false
14
14
  self.attribute_types = Hash.new(Type.default_value)
15
15
  self._default_attributes = AttributeSet.new({})
16
16
  end
17
17
 
18
18
  module ClassMethods
19
- def attribute(name, type = Type::Value.new, **options)
19
+ def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
20
20
  name = name.to_s
21
- if type.is_a?(Symbol)
22
- type = ActiveModel::Type.lookup(type, **options.except(:default))
23
- end
24
- self.attribute_types = attribute_types.merge(name => type)
25
- define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
21
+
22
+ cast_type = Type.lookup(cast_type, **options) if Symbol === cast_type
23
+ cast_type ||= attribute_types[name]
24
+
25
+ self.attribute_types = attribute_types.merge(name => cast_type)
26
+ define_default_attribute(name, default, cast_type)
26
27
  define_attribute_method(name)
27
28
  end
28
29
 
@@ -46,10 +47,12 @@ module ActiveModel
46
47
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
47
48
  owner, name, writer: true,
48
49
  ) do |temp_method_name, attr_name_expr|
49
- owner <<
50
- "def #{temp_method_name}(value)" <<
51
- " _write_attribute(#{attr_name_expr}, value)" <<
52
- "end"
50
+ owner.define_cached_method("#{name}=", as: temp_method_name, namespace: :active_model) do |batch|
51
+ batch <<
52
+ "def #{temp_method_name}(value)" <<
53
+ " _write_attribute(#{attr_name_expr}, value)" <<
54
+ "end"
55
+ end
53
56
  end
54
57
  end
55
58
 
@@ -63,7 +63,7 @@ module ActiveModel
63
63
  # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
64
64
  #
65
65
  module Callbacks
66
- def self.extended(base) #:nodoc:
66
+ def self.extended(base) # :nodoc:
67
67
  base.class_eval do
68
68
  include ActiveSupport::Callbacks
69
69
  end
@@ -96,10 +96,10 @@ module ActiveModel
96
96
  self.class._to_partial_path
97
97
  end
98
98
 
99
- module ClassMethods #:nodoc:
99
+ module ClassMethods # :nodoc:
100
100
  # Provide a class level cache for #to_partial_path. This is an
101
101
  # internal method and should not be accessed directly.
102
- def _to_partial_path #:nodoc:
102
+ def _to_partial_path # :nodoc:
103
103
  @_to_partial_path ||= begin
104
104
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
105
105
  collection = ActiveSupport::Inflector.tableize(name)
@@ -123,10 +123,11 @@ module ActiveModel
123
123
  include ActiveModel::AttributeMethods
124
124
 
125
125
  included do
126
- attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
127
- attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
128
- attribute_method_affix prefix: "restore_", suffix: "!"
129
- attribute_method_affix prefix: "clear_", suffix: "_change"
126
+ attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
127
+ attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
128
+ attribute_method_suffix "_previous_change", "_previously_was", parameters: false
129
+ attribute_method_affix prefix: "restore_", suffix: "!", parameters: false
130
+ attribute_method_affix prefix: "clear_", suffix: "_change", parameters: false
130
131
  end
131
132
 
132
133
  def initialize_dup(other) # :nodoc:
@@ -140,7 +141,7 @@ module ActiveModel
140
141
  end
141
142
 
142
143
  def as_json(options = {}) # :nodoc:
143
- options[:except] = [options[:except], "mutations_from_database"].flatten
144
+ options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
144
145
  super(options)
145
146
  end
146
147
 
@@ -63,12 +63,19 @@ module ActiveModel
63
63
  include Enumerable
64
64
 
65
65
  extend Forwardable
66
- def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq!, :any?
67
- # TODO: forward all enumerable methods after `each` deprecation is removed.
68
- def_delegators :@errors, :count
69
66
 
70
- LEGACY_ATTRIBUTES = [:messages, :details].freeze
71
- private_constant :LEGACY_ATTRIBUTES
67
+ # :method: each
68
+ #
69
+ # :call-seq: each(&block)
70
+ #
71
+ # Iterates through each error object.
72
+ #
73
+ # person.errors.add(:name, :too_short, count: 2)
74
+ # person.errors.each do |error|
75
+ # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
76
+ # options={:count=>3}>
77
+ # end
78
+ def_delegators :@errors, :each, :clear, :empty?, :size, :uniq!
72
79
 
73
80
  # The actual array of +Error+ objects
74
81
  # This method is aliased to <tt>objects</tt>.
@@ -133,30 +140,13 @@ module ActiveModel
133
140
  #
134
141
  # person.errors.merge!(other)
135
142
  def merge!(other)
143
+ return errors if equal?(other)
144
+
136
145
  other.errors.each { |error|
137
146
  import(error)
138
147
  }
139
148
  end
140
149
 
141
- # Removes all errors except the given keys. Returns a hash containing the removed errors.
142
- #
143
- # person.errors.keys # => [:name, :age, :gender, :city]
144
- # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
145
- # person.errors.keys # => [:age, :gender]
146
- def slice!(*keys)
147
- deprecation_removal_warning(:slice!)
148
-
149
- keys = keys.map(&:to_sym)
150
-
151
- results = messages.dup.slice!(*keys)
152
-
153
- @errors.keep_if do |error|
154
- keys.include?(error.attribute)
155
- end
156
-
157
- results
158
- end
159
-
160
150
  # Search for errors matching +attribute+, +type+ or +options+.
161
151
  #
162
152
  # Only supplied params will be matched.
@@ -205,76 +195,7 @@ module ActiveModel
205
195
  # person.errors[:name] # => ["cannot be nil"]
206
196
  # person.errors['name'] # => ["cannot be nil"]
207
197
  def [](attribute)
208
- DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
209
- end
210
-
211
- # Iterates through each error object.
212
- #
213
- # person.errors.add(:name, :too_short, count: 2)
214
- # person.errors.each do |error|
215
- # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
216
- # options={:count=>3}>
217
- # end
218
- #
219
- # To be backward compatible with past deprecated hash-like behavior,
220
- # when block accepts two parameters instead of one, it
221
- # iterates through each error key, value pair in the error messages hash.
222
- # Yields the attribute and the error for that attribute. If the attribute
223
- # has more than one error message, yields once for each error message.
224
- #
225
- # person.errors.add(:name, :blank, message: "can't be blank")
226
- # person.errors.each do |attribute, message|
227
- # # Will yield :name and "can't be blank"
228
- # end
229
- #
230
- # person.errors.add(:name, :not_specified, message: "must be specified")
231
- # person.errors.each do |attribute, message|
232
- # # Will yield :name and "can't be blank"
233
- # # then yield :name and "must be specified"
234
- # end
235
- def each(&block)
236
- if block.arity <= 1
237
- @errors.each(&block)
238
- else
239
- ActiveSupport::Deprecation.warn(<<~MSG)
240
- Enumerating ActiveModel::Errors as a hash has been deprecated.
241
- In Rails 6.1, `errors` is an array of Error objects,
242
- therefore it should be accessed by a block with a single block
243
- parameter like this:
244
-
245
- person.errors.each do |error|
246
- attribute = error.attribute
247
- message = error.message
248
- end
249
-
250
- You are passing a block expecting two parameters,
251
- so the old hash behavior is simulated. As this is deprecated,
252
- this will result in an ArgumentError in Rails 6.2.
253
- MSG
254
- @errors.
255
- sort { |a, b| a.attribute <=> b.attribute }.
256
- each { |error| yield error.attribute, error.message }
257
- end
258
- end
259
-
260
- # Returns all message values.
261
- #
262
- # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
263
- # person.errors.values # => [["cannot be nil", "must be specified"]]
264
- def values
265
- deprecation_removal_warning(:values, "errors.map { |error| error.message }")
266
- @errors.map(&:message).freeze
267
- end
268
-
269
- # Returns all message keys.
270
- #
271
- # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
272
- # person.errors.keys # => [:name]
273
- def keys
274
- deprecation_removal_warning(:keys, "errors.attribute_names")
275
- keys = @errors.map(&:attribute)
276
- keys.uniq!
277
- keys.freeze
198
+ messages_for(attribute)
278
199
  end
279
200
 
280
201
  # Returns all error attribute names
@@ -285,22 +206,6 @@ module ActiveModel
285
206
  @errors.map(&:attribute).uniq.freeze
286
207
  end
287
208
 
288
- # Returns an xml formatted representation of the Errors hash.
289
- #
290
- # person.errors.add(:name, :blank, message: "can't be blank")
291
- # person.errors.add(:name, :not_specified, message: "must be specified")
292
- # person.errors.to_xml
293
- # # =>
294
- # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
295
- # # <errors>
296
- # # <error>name can't be blank</error>
297
- # # <error>name must be specified</error>
298
- # # </errors>
299
- def to_xml(options = {})
300
- deprecation_removal_warning(:to_xml)
301
- to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
302
- end
303
-
304
209
  # Returns a Hash that can be used as the JSON representation for this
305
210
  # object. You can pass the <tt>:full_messages</tt> option. This determines
306
211
  # if the json object should contain full messages or not (false by default).
@@ -323,33 +228,26 @@ module ActiveModel
323
228
  end
324
229
  end
325
230
 
326
- def to_h
327
- ActiveSupport::Deprecation.warn(<<~EOM)
328
- ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
329
- Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
330
- returned by `ActiveModel::Errors.to_hash` is an array of error messages.
331
- EOM
231
+ undef :to_h
332
232
 
333
- to_hash.transform_values { |values| values.last }
334
- end
233
+ EMPTY_ARRAY = [].freeze # :nodoc:
335
234
 
336
235
  # Returns a Hash of attributes with an array of their error messages.
337
- #
338
- # Updating this hash would still update errors state for backward
339
- # compatibility, but this behavior is deprecated.
340
236
  def messages
341
- DeprecationHandlingMessageHash.new(self)
237
+ hash = to_hash
238
+ hash.default = EMPTY_ARRAY
239
+ hash.freeze
240
+ hash
342
241
  end
343
242
 
344
243
  # Returns a Hash of attributes with an array of their error details.
345
- #
346
- # Updating this hash would still update errors state for backward
347
- # compatibility, but this behavior is deprecated.
348
244
  def details
349
245
  hash = group_by_attribute.transform_values do |errors|
350
246
  errors.map(&:details)
351
247
  end
352
- DeprecationHandlingDetailsHash.new(hash)
248
+ hash.default = EMPTY_ARRAY
249
+ hash.freeze
250
+ hash
353
251
  end
354
252
 
355
253
  # Returns a Hash of attributes with an array of their Error objects.
@@ -378,6 +276,14 @@ module ActiveModel
378
276
  # If +type+ is a symbol, it will be translated using the appropriate
379
277
  # scope (see +generate_message+).
380
278
  #
279
+ # person.errors.add(:name, :blank)
280
+ # person.errors.messages
281
+ # # => {:name=>["can't be blank"]}
282
+ #
283
+ # person.errors.add(:name, :too_long, { count: 25 })
284
+ # person.errors.messages
285
+ # # => ["is too long (maximum is 25 characters)"]
286
+ #
381
287
  # If +type+ is a proc, it will be called, allowing for things like
382
288
  # <tt>Time.now</tt> to be used within an error.
383
289
  #
@@ -542,25 +448,10 @@ module ActiveModel
542
448
  Error.generate_message(attribute, type, @base, options)
543
449
  end
544
450
 
545
- def marshal_load(array) # :nodoc:
546
- # Rails 5
547
- @errors = []
548
- @base = array[0]
549
- add_from_legacy_details_hash(array[2])
550
- end
551
-
552
- def init_with(coder) # :nodoc:
553
- data = coder.map
451
+ def inspect # :nodoc:
452
+ inspection = @errors.inspect
554
453
 
555
- data.each { |k, v|
556
- next if LEGACY_ATTRIBUTES.include?(k.to_sym)
557
- instance_variable_set(:"@#{k}", v)
558
- }
559
-
560
- @errors ||= []
561
-
562
- # Legacy support Rails 5.x details hash
563
- add_from_legacy_details_hash(data["details"]) if data.key?("details")
454
+ "#<#{self.class.name} #{inspection}>"
564
455
  end
565
456
 
566
457
  private
@@ -572,97 +463,6 @@ module ActiveModel
572
463
 
573
464
  [attribute.to_sym, type, options]
574
465
  end
575
-
576
- def add_from_legacy_details_hash(details)
577
- details.each { |attribute, errors|
578
- errors.each { |error|
579
- type = error.delete(:error)
580
- add(attribute, type, **error)
581
- }
582
- }
583
- end
584
-
585
- def deprecation_removal_warning(method_name, alternative_message = nil)
586
- message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
587
- if alternative_message
588
- message << "\n\nTo achieve the same use:\n\n "
589
- message << alternative_message
590
- end
591
- ActiveSupport::Deprecation.warn(message)
592
- end
593
-
594
- def deprecation_rename_warning(old_method_name, new_method_name)
595
- ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
596
- end
597
- end
598
-
599
- class DeprecationHandlingMessageHash < SimpleDelegator
600
- def initialize(errors)
601
- @errors = errors
602
- super(prepare_content)
603
- end
604
-
605
- def []=(attribute, value)
606
- ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
607
-
608
- @errors.delete(attribute)
609
- Array(value).each do |message|
610
- @errors.add(attribute, message)
611
- end
612
-
613
- __setobj__ prepare_content
614
- end
615
-
616
- def delete(attribute)
617
- ActiveSupport::Deprecation.warn("Calling `delete` to an ActiveModel::Errors messages hash is deprecated. Please call `ActiveModel::Errors#delete` instead.")
618
-
619
- @errors.delete(attribute)
620
- end
621
-
622
- private
623
- def prepare_content
624
- content = @errors.to_hash
625
- content.each do |attribute, value|
626
- content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute)
627
- end
628
- content.default_proc = proc do |hash, attribute|
629
- hash = hash.dup
630
- hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute)
631
- __setobj__ hash.freeze
632
- hash[attribute]
633
- end
634
- content.freeze
635
- end
636
- end
637
-
638
- class DeprecationHandlingMessageArray < SimpleDelegator
639
- def initialize(content, errors, attribute)
640
- @errors = errors
641
- @attribute = attribute
642
- super(content.freeze)
643
- end
644
-
645
- def <<(message)
646
- ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
647
-
648
- @errors.add(@attribute, message)
649
- __setobj__ @errors.messages_for(@attribute)
650
- self
651
- end
652
-
653
- def clear
654
- ActiveSupport::Deprecation.warn("Calling `clear` to an ActiveModel::Errors message array in order to delete all errors is deprecated. Please call `ActiveModel::Errors#delete` instead.")
655
-
656
- @errors.delete(@attribute)
657
- end
658
- end
659
-
660
- class DeprecationHandlingDetailsHash < SimpleDelegator
661
- def initialize(details)
662
- details.default = []
663
- details.freeze
664
- super(details)
665
- end
666
466
  end
667
467
 
668
468
  # Raised when a validation cannot be corrected by end users and are considered
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 4
13
- PRE = "1"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "rc2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,5 +32,6 @@ en:
32
32
  less_than: "must be less than %{count}"
33
33
  less_than_or_equal_to: "must be less than or equal to %{count}"
34
34
  other_than: "must be other than %{count}"
35
+ in: "must be in %{count}"
35
36
  odd: "must be odd"
36
37
  even: "must be even"
@@ -3,11 +3,10 @@
3
3
  module ActiveModel
4
4
  # == Active \Model \Basic \Model
5
5
  #
6
- # Includes the required interface for an object to interact with
7
- # Action Pack and Action View, using different Active Model modules.
8
- # It includes model name introspections, conversions, translations and
9
- # validations. Besides that, it allows you to initialize the object with a
10
- # hash of attributes, pretty much like Active Record does.
6
+ # Allows implementing models similar to <tt>ActiveRecord::Base</tt>.
7
+ # Includes <tt>ActiveModel::API</tt> for the required interface for an
8
+ # object to interact with Action Pack and Action View, but can be
9
+ # extended with other functionalities.
11
10
  #
12
11
  # A minimal implementation could be:
13
12
  #
@@ -20,23 +19,7 @@ module ActiveModel
20
19
  # person.name # => "bob"
21
20
  # person.age # => "18"
22
21
  #
23
- # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
24
- # to return +false+, which is the most common case. You may want to override
25
- # it in your class to simulate a different scenario:
26
- #
27
- # class Person
28
- # include ActiveModel::Model
29
- # attr_accessor :id, :name
30
- #
31
- # def persisted?
32
- # self.id == 1
33
- # end
34
- # end
35
- #
36
- # person = Person.new(id: 1, name: 'bob')
37
- # person.persisted? # => true
38
- #
39
- # Also, if for some reason you need to run code on <tt>initialize</tt>, make
22
+ # If for some reason you need to run code on <tt>initialize</tt>, make
40
23
  # sure you call +super+ if you want the attributes hash initialization to
41
24
  # happen.
42
25
  #
@@ -58,42 +41,6 @@ module ActiveModel
58
41
  # (see below).
59
42
  module Model
60
43
  extend ActiveSupport::Concern
61
- include ActiveModel::AttributeAssignment
62
- include ActiveModel::Validations
63
- include ActiveModel::Conversion
64
-
65
- included do
66
- extend ActiveModel::Naming
67
- extend ActiveModel::Translation
68
- end
69
-
70
- # Initializes a new model with the given +params+.
71
- #
72
- # class Person
73
- # include ActiveModel::Model
74
- # attr_accessor :name, :age
75
- # end
76
- #
77
- # person = Person.new(name: 'bob', age: '18')
78
- # person.name # => "bob"
79
- # person.age # => "18"
80
- def initialize(attributes = {})
81
- assign_attributes(attributes) if attributes
82
-
83
- super()
84
- end
85
-
86
- # Indicates if the model is persisted. Default is +false+.
87
- #
88
- # class Person
89
- # include ActiveModel::Model
90
- # attr_accessor :id, :name
91
- # end
92
- #
93
- # person = Person.new(id: 1, name: 'bob')
94
- # person.persisted? # => false
95
- def persisted?
96
- false
97
- end
44
+ include ActiveModel::API
98
45
  end
99
46
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/hash/except"
4
4
  require "active_support/core_ext/module/introspection"
5
5
  require "active_support/core_ext/module/redefine_method"
6
+ require "active_support/core_ext/module/delegation"
6
7
 
7
8
  module ActiveModel
8
9
  class Name
@@ -153,6 +154,7 @@ module ActiveModel
153
154
  # Returns a new ActiveModel::Name instance. By default, the +namespace+
154
155
  # and +name+ option will take the namespace and name of the given class
155
156
  # respectively.
157
+ # Use +locale+ argument for singularize and pluralize model name.
156
158
  #
157
159
  # module Foo
158
160
  # class Bar
@@ -161,7 +163,7 @@ module ActiveModel
161
163
  #
162
164
  # ActiveModel::Name.new(Foo::Bar).to_s
163
165
  # # => "Foo::Bar"
164
- def initialize(klass, namespace = nil, name = nil)
166
+ def initialize(klass, namespace = nil, name = nil, locale = :en)
165
167
  @name = name || klass.name
166
168
 
167
169
  raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
@@ -169,16 +171,17 @@ module ActiveModel
169
171
  @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
170
172
  @klass = klass
171
173
  @singular = _singularize(@name)
172
- @plural = ActiveSupport::Inflector.pluralize(@singular)
174
+ @plural = ActiveSupport::Inflector.pluralize(@singular, locale)
175
+ @uncountable = @plural == @singular
173
176
  @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
174
177
  @human = ActiveSupport::Inflector.humanize(@element)
175
178
  @collection = ActiveSupport::Inflector.tableize(@name)
176
179
  @param_key = (namespace ? _singularize(@unnamespaced) : @singular)
177
180
  @i18n_key = @name.underscore.to_sym
178
181
 
179
- @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
180
- @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
181
- @route_key << "_index" if @plural == @singular
182
+ @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key, locale) : @plural.dup)
183
+ @singular_route_key = ActiveSupport::Inflector.singularize(@route_key, locale)
184
+ @route_key << "_index" if @uncountable
182
185
  end
183
186
 
184
187
  # Transform the model name into a more human format, using I18n. By default,
@@ -206,6 +209,10 @@ module ActiveModel
206
209
  I18n.translate(defaults.shift, **options)
207
210
  end
208
211
 
212
+ def uncountable?
213
+ @uncountable
214
+ end
215
+
209
216
  private
210
217
  def _singularize(string)
211
218
  ActiveSupport::Inflector.underscore(string).tr("/", "_")
@@ -232,7 +239,7 @@ module ActiveModel
232
239
  # is required to pass the \Active \Model Lint test. So either extending the
233
240
  # provided method below, or rolling your own is required.
234
241
  module Naming
235
- def self.extended(base) #:nodoc:
242
+ def self.extended(base) # :nodoc:
236
243
  base.silence_redefinition_of_method :model_name
237
244
  base.delegate :model_name, to: :class
238
245
  end
@@ -279,7 +286,7 @@ module ActiveModel
279
286
  # ActiveModel::Naming.uncountable?(Sheep) # => true
280
287
  # ActiveModel::Naming.uncountable?(Post) # => false
281
288
  def self.uncountable?(record_or_class)
282
- plural(record_or_class) == singular(record_or_class)
289
+ model_name_from_record_or_class(record_or_class).uncountable?
283
290
  end
284
291
 
285
292
  # Returns string to use while generating route names. It differs for
@@ -321,7 +328,7 @@ module ActiveModel
321
328
  model_name_from_record_or_class(record_or_class).param_key
322
329
  end
323
330
 
324
- def self.model_name_from_record_or_class(record_or_class) #:nodoc:
331
+ def self.model_name_from_record_or_class(record_or_class) # :nodoc:
325
332
  if record_or_class.respond_to?(:to_model)
326
333
  record_or_class.to_model.model_name
327
334
  else
@@ -94,6 +94,7 @@ module ActiveModel
94
94
 
95
95
  define_method("#{attribute}=") do |unencrypted_password|
96
96
  if unencrypted_password.nil?
97
+ instance_variable_set("@#{attribute}", nil)
97
98
  self.public_send("#{attribute}_digest=", nil)
98
99
  elsif !unencrypted_password.empty?
99
100
  instance_variable_set("@#{attribute}", unencrypted_password)
@@ -118,7 +119,7 @@ module ActiveModel
118
119
  # user.authenticate_password('mUc3m00RsqyRe') # => user
119
120
  define_method("authenticate_#{attribute}") do |unencrypted_password|
120
121
  attribute_digest = public_send("#{attribute}_digest")
121
- BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
122
+ attribute_digest.present? && BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
122
123
  end
123
124
 
124
125
  alias_method :authenticate, :authenticate_password if attribute == :password
@@ -123,7 +123,7 @@ module ActiveModel
123
123
  # user.serializable_hash(include: { notes: { only: 'title' }})
124
124
  # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
125
125
  def serializable_hash(options = nil)
126
- attribute_names = attributes.keys
126
+ attribute_names = self.attribute_names
127
127
 
128
128
  return serializable_attributes(attribute_names) if options.blank?
129
129
 
@@ -148,6 +148,11 @@ module ActiveModel
148
148
  hash
149
149
  end
150
150
 
151
+ # Returns an array of attribute names as strings
152
+ def attribute_names # :nodoc:
153
+ attributes.keys
154
+ end
155
+
151
156
  private
152
157
  # Hook method defining how an attribute value should be retrieved for
153
158
  # serialization. By default this is assumed to be an instance named after
@@ -177,7 +182,7 @@ module ActiveModel
177
182
  # +association+ - name of the association
178
183
  # +records+ - the association record(s) to be serialized
179
184
  # +opts+ - options for the association records
180
- def serializable_add_includes(options = {}) #:nodoc:
185
+ def serializable_add_includes(options = {}) # :nodoc:
181
186
  return unless includes = options[:include]
182
187
 
183
188
  unless includes.is_a?(Hash)