activemodel 7.0.6 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -132
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +26 -1
  9. data/lib/active_model/attribute_assignment.rb +1 -1
  10. data/lib/active_model/attribute_methods.rb +103 -64
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +10 -1
  13. data/lib/active_model/attributes.rb +62 -45
  14. data/lib/active_model/callbacks.rb +5 -5
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +5 -4
  19. data/lib/active_model/errors.rb +37 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +4 -4
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +34 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +61 -23
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +7 -1
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/mutable.rb +4 -4
  40. data/lib/active_model/type/helpers/numeric.rb +6 -1
  41. data/lib/active_model/type/helpers/time_value.rb +28 -12
  42. data/lib/active_model/type/immutable_string.rb +37 -1
  43. data/lib/active_model/type/integer.rb +44 -1
  44. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  45. data/lib/active_model/type/string.rb +9 -1
  46. data/lib/active_model/type/time.rb +48 -7
  47. data/lib/active_model/type/value.rb +23 -3
  48. data/lib/active_model/type.rb +1 -0
  49. data/lib/active_model/validations/absence.rb +1 -1
  50. data/lib/active_model/validations/acceptance.rb +1 -1
  51. data/lib/active_model/validations/callbacks.rb +4 -4
  52. data/lib/active_model/validations/clusivity.rb +5 -8
  53. data/lib/active_model/validations/comparability.rb +0 -11
  54. data/lib/active_model/validations/comparison.rb +15 -7
  55. data/lib/active_model/validations/format.rb +6 -7
  56. data/lib/active_model/validations/length.rb +10 -8
  57. data/lib/active_model/validations/numericality.rb +35 -23
  58. data/lib/active_model/validations/presence.rb +1 -1
  59. data/lib/active_model/validations/resolve_value.rb +26 -0
  60. data/lib/active_model/validations/validates.rb +4 -4
  61. data/lib/active_model/validations/with.rb +9 -2
  62. data/lib/active_model/validations.rb +44 -9
  63. data/lib/active_model/validator.rb +7 -5
  64. data/lib/active_model/version.rb +1 -1
  65. data/lib/active_model.rb +5 -1
  66. metadata +16 -11
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # == Active \Model \Conversion
4
+ # = Active \Model \Conversion
5
5
  #
6
6
  # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
7
7
  #
@@ -24,6 +24,14 @@ module ActiveModel
24
24
  module Conversion
25
25
  extend ActiveSupport::Concern
26
26
 
27
+ included do
28
+ ##
29
+ # :singleton-method:
30
+ #
31
+ # Accepts a string that will be used as a delimiter of object's key values in the `to_param` method.
32
+ class_attribute :param_delimiter, instance_reader: false, default: "-"
33
+ end
34
+
27
35
  # If your object is already designed to implement all of the \Active \Model
28
36
  # you can use the default <tt>:to_model</tt> implementation, which simply
29
37
  # returns +self+.
@@ -58,7 +66,7 @@ module ActiveModel
58
66
  # person.to_key # => [1]
59
67
  def to_key
60
68
  key = respond_to?(:id) && id
61
- key ? [key] : nil
69
+ key ? Array(key) : nil
62
70
  end
63
71
 
64
72
  # Returns a +string+ representing the object's key suitable for use in URLs,
@@ -80,7 +88,7 @@ module ActiveModel
80
88
  # person = Person.new(1)
81
89
  # person.to_param # => "1"
82
90
  def to_param
83
- (persisted? && key = to_key) ? key.join("-") : nil
91
+ (persisted? && (key = to_key) && key.all?) ? key.join(self.class.param_delimiter) : nil
84
92
  end
85
93
 
86
94
  # Returns a +string+ identifying the path associated with the object.
@@ -100,7 +108,9 @@ module ActiveModel
100
108
  # Provide a class level cache for #to_partial_path. This is an
101
109
  # internal method and should not be accessed directly.
102
110
  def _to_partial_path # :nodoc:
103
- @_to_partial_path ||= begin
111
+ @_to_partial_path ||= if respond_to?(:model_name)
112
+ "#{model_name.collection}/#{model_name.element}"
113
+ else
104
114
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
105
115
  collection = ActiveSupport::Inflector.tableize(name)
106
116
  "#{collection}/#{element}"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -3,7 +3,7 @@
3
3
  require "active_model/attribute_mutation_tracker"
4
4
 
5
5
  module ActiveModel
6
- # == Active \Model \Dirty
6
+ # = Active \Model \Dirty
7
7
  #
8
8
  # Provides a way to track changes in your object in the same way as
9
9
  # Active Record does.
@@ -13,8 +13,7 @@ module ActiveModel
13
13
  # * <tt>include ActiveModel::Dirty</tt> in your object.
14
14
  # * Call <tt>define_attribute_methods</tt> passing each method you want to
15
15
  # track.
16
- # * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked
17
- # attribute.
16
+ # * Call <tt>*_will_change!</tt> before each change to the tracked attribute.
18
17
  # * Call <tt>changes_applied</tt> after the changes are persisted.
19
18
  # * Call <tt>clear_changes_information</tt> when you want to reset the changes
20
19
  # information.
@@ -109,20 +108,136 @@ module ActiveModel
109
108
  # person.changes # => {"name" => ["Bill", "Bob"]}
110
109
  #
111
110
  # If an attribute is modified in-place then make use of
112
- # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
111
+ # {*_will_change!}[rdoc-label:method-i-2A_will_change-21] to mark that the attribute is changing.
113
112
  # Otherwise \Active \Model can't track changes to in-place attributes. Note
114
113
  # that Active Record can detect in-place modifications automatically. You do
115
- # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
114
+ # not need to call <tt>*_will_change!</tt> on Active Record models.
116
115
  #
117
116
  # person.name_will_change!
118
117
  # person.name_change # => ["Bill", "Bill"]
119
118
  # person.name << 'y'
120
119
  # person.name_change # => ["Bill", "Billy"]
120
+ #
121
+ # Methods can be invoked as +name_changed?+ or by passing an argument to the
122
+ # generic method <tt>attribute_changed?("name")</tt>.
121
123
  module Dirty
122
124
  extend ActiveSupport::Concern
123
125
  include ActiveModel::AttributeMethods
124
126
 
125
127
  included do
128
+ ##
129
+ # :method: *_previously_changed?
130
+ #
131
+ # :call-seq: *_previously_changed?(**options)
132
+ #
133
+ # This method is generated for each attribute.
134
+ #
135
+ # Returns true if the attribute previously had unsaved changes.
136
+ #
137
+ # person = Person.new
138
+ # person.name = 'Britanny'
139
+ # person.save
140
+ # person.name_previously_changed? # => true
141
+ # person.name_previously_changed?(from: nil, to: 'Britanny') # => true
142
+
143
+ ##
144
+ # :method: *_changed?
145
+ #
146
+ # This method is generated for each attribute.
147
+ #
148
+ # Returns true if the attribute has unsaved changes.
149
+ #
150
+ # person = Person.new
151
+ # person.name = 'Andrew'
152
+ # person.name_changed? # => true
153
+
154
+ ##
155
+ # :method: *_change
156
+ #
157
+ # This method is generated for each attribute.
158
+ #
159
+ # Returns the old and the new value of the attribute.
160
+ #
161
+ # person = Person.new
162
+ # person.name = 'Nick'
163
+ # person.name_change # => [nil, 'Nick']
164
+
165
+ ##
166
+ # :method: *_will_change!
167
+ #
168
+ # This method is generated for each attribute.
169
+ #
170
+ # If an attribute is modified in-place then make use of
171
+ # <tt>*_will_change!</tt> to mark that the attribute is changing.
172
+ # Otherwise Active Model can’t track changes to in-place attributes. Note
173
+ # that Active Record can detect in-place modifications automatically. You
174
+ # do not need to call <tt>*_will_change!</tt> on Active Record
175
+ # models.
176
+ #
177
+ # person = Person.new('Sandy')
178
+ # person.name_will_change!
179
+ # person.name_change # => ['Sandy', 'Sandy']
180
+
181
+ ##
182
+ # :method: *_was
183
+ #
184
+ # This method is generated for each attribute.
185
+ #
186
+ # Returns the old value of the attribute.
187
+ #
188
+ # person = Person.new(name: 'Steph')
189
+ # person.name = 'Stephanie'
190
+ # person.name_was # => 'Steph'
191
+
192
+ ##
193
+ # :method: *_previous_change
194
+ #
195
+ # This method is generated for each attribute.
196
+ #
197
+ # Returns the old and the new value of the attribute before the last save.
198
+ #
199
+ # person = Person.new
200
+ # person.name = 'Emmanuel'
201
+ # person.save
202
+ # person.name_previous_change # => [nil, 'Emmanuel']
203
+
204
+ ##
205
+ # :method: *_previously_was
206
+ #
207
+ # This method is generated for each attribute.
208
+ #
209
+ # Returns the old value of the attribute before the last save.
210
+ #
211
+ # person = Person.new
212
+ # person.name = 'Sage'
213
+ # person.save
214
+ # person.name_previously_was # => nil
215
+
216
+ ##
217
+ # :method: restore_*!
218
+ #
219
+ # This method is generated for each attribute.
220
+ #
221
+ # Restores the attribute to the old value.
222
+ #
223
+ # person = Person.new
224
+ # person.name = 'Amanda'
225
+ # person.restore_name!
226
+ # person.name # => nil
227
+
228
+ ##
229
+ # :method: clear_*_change
230
+ #
231
+ # This method is generated for each attribute.
232
+ #
233
+ # Clears all dirty data of the attribute: current changes and previous changes.
234
+ #
235
+ # person = Person.new(name: 'Chris')
236
+ # person.name = 'Jason'
237
+ # person.name_change # => ['Chris', 'Jason']
238
+ # person.clear_name_change
239
+ # person.name_change # => nil
240
+
126
241
  attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
127
242
  attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
128
243
  attribute_method_suffix "_previous_change", "_previously_was", parameters: false
@@ -174,23 +289,23 @@ module ActiveModel
174
289
  mutations_from_database.changed_attribute_names
175
290
  end
176
291
 
177
- # Dispatch target for <tt>*_changed?</tt> attribute methods.
178
- def attribute_changed?(attr_name, **options) # :nodoc:
292
+ # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
293
+ def attribute_changed?(attr_name, **options)
179
294
  mutations_from_database.changed?(attr_name.to_s, **options)
180
295
  end
181
296
 
182
- # Dispatch target for <tt>*_was</tt> attribute methods.
183
- def attribute_was(attr_name) # :nodoc:
297
+ # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
298
+ def attribute_was(attr_name)
184
299
  mutations_from_database.original_value(attr_name.to_s)
185
300
  end
186
301
 
187
- # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
188
- def attribute_previously_changed?(attr_name, **options) # :nodoc:
302
+ # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
303
+ def attribute_previously_changed?(attr_name, **options)
189
304
  mutations_before_last_save.changed?(attr_name.to_s, **options)
190
305
  end
191
306
 
192
- # Dispatch target for <tt>*_previously_was</tt> attribute methods.
193
- def attribute_previously_was(attr_name) # :nodoc:
307
+ # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
308
+ def attribute_previously_was(attr_name)
194
309
  mutations_before_last_save.original_value(attr_name.to_s)
195
310
  end
196
311
 
@@ -247,6 +362,12 @@ module ActiveModel
247
362
  end
248
363
 
249
364
  private
365
+ def init_internals
366
+ super
367
+ @mutations_before_last_save = nil
368
+ @mutations_from_database = nil
369
+ end
370
+
250
371
  def clear_attribute_change(attr_name)
251
372
  mutations_from_database.forget_change(attr_name.to_s)
252
373
  end
@@ -3,7 +3,7 @@
3
3
  require "active_support/core_ext/class/attribute"
4
4
 
5
5
  module ActiveModel
6
- # == Active \Model \Error
6
+ # = Active \Model \Error
7
7
  #
8
8
  # Represents one single error
9
9
  class Error
@@ -49,7 +49,7 @@ module ActiveModel
49
49
  defaults << :"errors.format"
50
50
  defaults << "%{attribute} %{message}"
51
51
 
52
- attr_name = attribute.tr(".", "_").humanize
52
+ attr_name = attribute.remove(/\.base\z/).tr(".", "_").humanize
53
53
  attr_name = base_class.human_attribute_name(attribute, {
54
54
  default: attr_name,
55
55
  base: base,
@@ -121,9 +121,10 @@ module ActiveModel
121
121
  attr_reader :attribute
122
122
  # The type of error, defaults to +:invalid+ unless specified
123
123
  attr_reader :type
124
- # The raw value provided as the second parameter when calling +errors#add+
124
+ # The raw value provided as the second parameter when calling
125
+ # <tt>errors#add</tt>
125
126
  attr_reader :raw_type
126
- # The options provided when calling +errors#add+
127
+ # The options provided when calling <tt>errors#add</tt>
127
128
  attr_reader :options
128
129
 
129
130
  # Returns the error message.
@@ -3,13 +3,12 @@
3
3
  require "active_support/core_ext/array/conversions"
4
4
  require "active_support/core_ext/string/inflections"
5
5
  require "active_support/core_ext/object/deep_dup"
6
- require "active_support/core_ext/string/filters"
7
6
  require "active_model/error"
8
7
  require "active_model/nested_error"
9
8
  require "forwardable"
10
9
 
11
10
  module ActiveModel
12
- # == Active \Model \Errors
11
+ # = Active \Model \Errors
13
12
  #
14
13
  # Provides error related functionalities you can include in your object
15
14
  # for handling error messages and interacting with Action View helpers.
@@ -64,6 +63,7 @@ module ActiveModel
64
63
 
65
64
  extend Forwardable
66
65
 
66
+ ##
67
67
  # :method: each
68
68
  #
69
69
  # :call-seq: each(&block)
@@ -75,6 +75,31 @@ module ActiveModel
75
75
  # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
76
76
  # options={:count=>3}>
77
77
  # end
78
+
79
+ ##
80
+ # :method: clear
81
+ #
82
+ # :call-seq: clear
83
+ #
84
+ # Clears all errors. Clearing the errors does not, however, make the model
85
+ # valid. The next time the validations are run (for example, via
86
+ # ActiveRecord::Validations#valid?), the errors collection will be filled
87
+ # again if any validations fail.
88
+
89
+ ##
90
+ # :method: empty?
91
+ #
92
+ # :call-seq: empty?
93
+ #
94
+ # Returns true if there are no errors.
95
+
96
+ ##
97
+ # :method: size
98
+ #
99
+ # :call-seq: size
100
+ #
101
+ # Returns number of errors.
102
+
78
103
  def_delegators :@errors, :each, :clear, :empty?, :size, :uniq!
79
104
 
80
105
  # The actual array of +Error+ objects
@@ -215,7 +240,7 @@ module ActiveModel
215
240
 
216
241
  # Returns a Hash that can be used as the JSON representation for this
217
242
  # object. You can pass the <tt>:full_messages</tt> option. This determines
218
- # if the json object should contain full messages or not (false by default).
243
+ # if the JSON object should contain full messages or not (false by default).
219
244
  #
220
245
  # person.errors.as_json # => {:name=>["cannot be nil"]}
221
246
  # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
@@ -287,7 +312,7 @@ module ActiveModel
287
312
  # person.errors.messages
288
313
  # # => {:name=>["can't be blank"]}
289
314
  #
290
- # person.errors.add(:name, :too_long, { count: 25 })
315
+ # person.errors.add(:name, :too_long, count: 25)
291
316
  # person.errors.messages
292
317
  # # => ["is too long (maximum is 25 characters)"]
293
318
  #
@@ -338,7 +363,7 @@ module ActiveModel
338
363
  # If the error requires options, then it returns +true+ with
339
364
  # the correct options, or +false+ with incorrect or missing options.
340
365
  #
341
- # person.errors.add :name, :too_long, { count: 25 }
366
+ # person.errors.add :name, :too_long, count: 25
342
367
  # person.errors.added? :name, :too_long, count: 25 # => true
343
368
  # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
344
369
  # person.errors.added? :name, :too_long, count: 24 # => false
@@ -360,7 +385,7 @@ module ActiveModel
360
385
  # present, or +false+ otherwise. +type+ is treated the same as for +add+.
361
386
  #
362
387
  # person.errors.add :age
363
- # person.errors.add :name, :too_long, { count: 25 }
388
+ # person.errors.add :name, :too_long, count: 25
364
389
  # person.errors.of_kind? :age # => true
365
390
  # person.errors.of_kind? :name # => false
366
391
  # person.errors.of_kind? :name, :too_long # => true
@@ -472,6 +497,8 @@ module ActiveModel
472
497
  end
473
498
  end
474
499
 
500
+ # = Active \Model \StrictValidationFailed
501
+ #
475
502
  # Raised when a validation cannot be corrected by end users and are considered
476
503
  # exceptional.
477
504
  #
@@ -490,10 +517,14 @@ module ActiveModel
490
517
  class StrictValidationFailed < StandardError
491
518
  end
492
519
 
520
+ # = Active \Model \RangeError
521
+ #
493
522
  # Raised when attribute values are out of range.
494
523
  class RangeError < ::RangeError
495
524
  end
496
525
 
526
+ # = Active \Model \UnknownAttributeError
527
+ #
497
528
  # Raised when unknown attributes are supplied via mass assignment.
498
529
  #
499
530
  # class Person
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
+ # = Active \Model \ForbiddenAttributesError
5
+ #
4
6
  # Raised when forbidden attributes are used for mass assignment.
5
7
  #
6
8
  # class Person < ActiveRecord::Base
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # Returns the currently loaded version of \Active \Model as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of \Active \Model as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 0
12
- TINY = 6
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 3
13
+ PRE = "4"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -5,7 +5,7 @@ module ActiveModel
5
5
  # == Active \Model \Lint \Tests
6
6
  #
7
7
  # You can test whether an object is compliant with the Active \Model API by
8
- # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will
8
+ # including +ActiveModel::Lint::Tests+ in your TestCase. It will
9
9
  # include tests that tell you whether your object is fully compliant,
10
10
  # or if not, which aspects of the API are not implemented.
11
11
  #
@@ -18,6 +18,7 @@ en:
18
18
  too_long:
19
19
  one: "is too long (maximum is 1 character)"
20
20
  other: "is too long (maximum is %{count} characters)"
21
+ password_too_long: "is too long"
21
22
  too_short:
22
23
  one: "is too short (minimum is 1 character)"
23
24
  other: "is too short (minimum is %{count} characters)"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # == Active \Model \Basic \Model
4
+ # = Active \Model \Basic \Model
5
5
  #
6
6
  # Allows implementing models similar to ActiveRecord::Base.
7
7
  # Includes ActiveModel::API for the required interface for an
@@ -37,10 +37,42 @@ module ActiveModel
37
37
  # person.omg # => true
38
38
  #
39
39
  # For more detailed information on other functionalities available, please
40
- # refer to the specific modules included in <tt>ActiveModel::Model</tt>
40
+ # refer to the specific modules included in +ActiveModel::Model+
41
41
  # (see below).
42
42
  module Model
43
43
  extend ActiveSupport::Concern
44
44
  include ActiveModel::API
45
+ include ActiveModel::Access
46
+
47
+ ##
48
+ # :method: slice
49
+ #
50
+ # :call-seq: slice(*methods)
51
+ #
52
+ # Returns a hash of the given methods with their names as keys and returned
53
+ # values as values.
54
+ #
55
+ # person = Person.new(id: 1, name: "bob")
56
+ # person.slice(:id, :name)
57
+ # => { "id" => 1, "name" => "bob" }
58
+ #
59
+ #--
60
+ # Implemented by ActiveModel::Access#slice.
61
+
62
+ ##
63
+ # :method: values_at
64
+ #
65
+ # :call-seq: values_at(*methods)
66
+ #
67
+ # Returns an array of the values returned by the given methods.
68
+ #
69
+ # person = Person.new(id: 1, name: "bob")
70
+ # person.values_at(:id, :name)
71
+ # => [1, "bob"]
72
+ #
73
+ #--
74
+ # Implemented by ActiveModel::Access#values_at.
45
75
  end
76
+
77
+ ActiveSupport.run_load_hooks(:active_model, Model)
46
78
  end
@@ -195,18 +195,15 @@ module ActiveModel
195
195
  #
196
196
  # Specify +options+ with additional translating options.
197
197
  def human(options = {})
198
- return @human unless @klass.respond_to?(:lookup_ancestors) &&
199
- @klass.respond_to?(:i18n_scope)
200
-
201
- defaults = @klass.lookup_ancestors.map do |klass|
202
- klass.model_name.i18n_key
203
- end
198
+ return @human if i18n_keys.empty? || i18n_scope.empty?
204
199
 
200
+ key, *defaults = i18n_keys
205
201
  defaults << options[:default] if options[:default]
206
- defaults << @human
202
+ defaults << MISSING_TRANSLATION
207
203
 
208
- options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
209
- I18n.translate(defaults.shift, **options)
204
+ translation = I18n.translate(key, scope: i18n_scope, count: 1, **options, default: defaults)
205
+ translation = @human if translation == MISSING_TRANSLATION
206
+ translation
210
207
  end
211
208
 
212
209
  def uncountable?
@@ -214,12 +211,26 @@ module ActiveModel
214
211
  end
215
212
 
216
213
  private
214
+ MISSING_TRANSLATION = -(2**60) # :nodoc:
215
+
217
216
  def _singularize(string)
218
217
  ActiveSupport::Inflector.underscore(string).tr("/", "_")
219
218
  end
219
+
220
+ def i18n_keys
221
+ @i18n_keys ||= if @klass.respond_to?(:lookup_ancestors)
222
+ @klass.lookup_ancestors.map { |klass| klass.model_name.i18n_key }
223
+ else
224
+ []
225
+ end
226
+ end
227
+
228
+ def i18n_scope
229
+ @i18n_scope ||= @klass.respond_to?(:i18n_scope) ? [@klass.i18n_scope, :models] : []
230
+ end
220
231
  end
221
232
 
222
- # == Active \Model \Naming
233
+ # = Active \Model \Naming
223
234
  #
224
235
  # Creates a +model_name+ method on your object.
225
236
  #
@@ -336,5 +347,13 @@ module ActiveModel
336
347
  end
337
348
  end
338
349
  private_class_method :model_name_from_record_or_class
350
+
351
+ private
352
+ def inherited(base)
353
+ super
354
+ base.class_eval do
355
+ @_model_name = nil
356
+ end
357
+ end
339
358
  end
340
359
  end
@@ -9,6 +9,10 @@ module ActiveModel
9
9
 
10
10
  config.active_model = ActiveSupport::OrderedOptions.new
11
11
 
12
+ initializer "active_model.deprecator", before: :load_environment_config do |app|
13
+ app.deprecators[:active_model] = ActiveModel.deprecator
14
+ end
15
+
12
16
  initializer "active_model.secure_password" do
13
17
  ActiveModel::SecurePassword.min_cost = Rails.env.test?
14
18
  end