activemodel 6.1.3.1 → 7.0.0.alpha1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -72
  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 +99 -52
  8. data/lib/active_model/attribute_set/builder.rb +1 -1
  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 +10 -4
  14. data/lib/active_model/errors.rb +20 -6
  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/serialization.rb +7 -2
  20. data/lib/active_model/translation.rb +1 -1
  21. data/lib/active_model/type/helpers/numeric.rb +9 -1
  22. data/lib/active_model/type/helpers/time_value.rb +2 -2
  23. data/lib/active_model/type/integer.rb +4 -1
  24. data/lib/active_model/type/registry.rb +9 -41
  25. data/lib/active_model/type/time.rb +1 -1
  26. data/lib/active_model/type.rb +6 -5
  27. data/lib/active_model/validations/absence.rb +1 -1
  28. data/lib/active_model/validations/clusivity.rb +1 -1
  29. data/lib/active_model/validations/comparability.rb +29 -0
  30. data/lib/active_model/validations/comparison.rb +82 -0
  31. data/lib/active_model/validations/confirmation.rb +4 -4
  32. data/lib/active_model/validations/numericality.rb +27 -20
  33. data/lib/active_model/validations.rb +4 -4
  34. data/lib/active_model/validator.rb +2 -2
  35. data/lib/active_model.rb +2 -1
  36. metadata +14 -11
@@ -146,7 +146,7 @@ module ActiveModel
146
146
  def marshal_load(values)
147
147
  if values.is_a?(Hash)
148
148
  ActiveSupport::Deprecation.warn(<<~MSG)
149
- Marshalling load from legacy attributes format is deprecated and will be removed in Rails 6.2.
149
+ Marshalling load from legacy attributes format is deprecated and will be removed in Rails 7.0.
150
150
  MSG
151
151
  empty_hash = {}.freeze
152
152
  initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
@@ -25,6 +25,10 @@ module ActiveModel
25
25
  attributes.transform_values(&:value_before_type_cast)
26
26
  end
27
27
 
28
+ def values_for_database
29
+ attributes.transform_values(&:value_for_database)
30
+ end
31
+
28
32
  def to_hash
29
33
  keys.index_with { |name| self[name].value }
30
34
  end
@@ -54,7 +58,6 @@ module ActiveModel
54
58
 
55
59
  def write_cast_value(name, value)
56
60
  @attributes[name] = self[name].with_cast_value(value)
57
- value
58
61
  end
59
62
 
60
63
  def freeze
@@ -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:
@@ -139,6 +140,11 @@ module ActiveModel
139
140
  @mutations_from_database = nil
140
141
  end
141
142
 
143
+ def as_json(options = {}) # :nodoc:
144
+ options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
145
+ super(options)
146
+ end
147
+
142
148
  # Clears dirty data and moves +changes+ to +previous_changes+ and
143
149
  # +mutations_from_database+ to +mutations_before_last_save+ respectively.
144
150
  def changes_applied
@@ -249,7 +249,7 @@ module ActiveModel
249
249
 
250
250
  You are passing a block expecting two parameters,
251
251
  so the old hash behavior is simulated. As this is deprecated,
252
- this will result in an ArgumentError in Rails 6.2.
252
+ this will result in an ArgumentError in Rails 7.0.
253
253
  MSG
254
254
  @errors.
255
255
  sort { |a, b| a.attribute <=> b.attribute }.
@@ -325,7 +325,7 @@ module ActiveModel
325
325
 
326
326
  def to_h
327
327
  ActiveSupport::Deprecation.warn(<<~EOM)
328
- ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
328
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 7.0.
329
329
  Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
330
330
  returned by `ActiveModel::Errors.to_hash` is an array of error messages.
331
331
  EOM
@@ -378,6 +378,14 @@ module ActiveModel
378
378
  # If +type+ is a symbol, it will be translated using the appropriate
379
379
  # scope (see +generate_message+).
380
380
  #
381
+ # person.errors.add(:name, :blank)
382
+ # person.errors.messages
383
+ # # => {:name=>["can't be blank"]}
384
+ #
385
+ # person.errors.add(:name, :too_long, { count: 25 })
386
+ # person.errors.messages
387
+ # # => ["is too long (maximum is 25 characters)"]
388
+ #
381
389
  # If +type+ is a proc, it will be called, allowing for things like
382
390
  # <tt>Time.now</tt> to be used within an error.
383
391
  #
@@ -563,6 +571,12 @@ module ActiveModel
563
571
  add_from_legacy_details_hash(data["details"]) if data.key?("details")
564
572
  end
565
573
 
574
+ def inspect # :nodoc:
575
+ inspection = @errors.inspect
576
+
577
+ "#<#{self.class.name} #{inspection}>"
578
+ end
579
+
566
580
  private
567
581
  def normalize_arguments(attribute, type, **options)
568
582
  # Evaluate proc first
@@ -583,7 +597,7 @@ module ActiveModel
583
597
  end
584
598
 
585
599
  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."
600
+ message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 7.0."
587
601
  if alternative_message
588
602
  message << "\n\nTo achieve the same use:\n\n "
589
603
  message << alternative_message
@@ -596,7 +610,7 @@ module ActiveModel
596
610
  end
597
611
  end
598
612
 
599
- class DeprecationHandlingMessageHash < SimpleDelegator
613
+ class DeprecationHandlingMessageHash < SimpleDelegator # :nodoc:
600
614
  def initialize(errors)
601
615
  @errors = errors
602
616
  super(prepare_content)
@@ -635,7 +649,7 @@ module ActiveModel
635
649
  end
636
650
  end
637
651
 
638
- class DeprecationHandlingMessageArray < SimpleDelegator
652
+ class DeprecationHandlingMessageArray < SimpleDelegator # :nodoc:
639
653
  def initialize(content, errors, attribute)
640
654
  @errors = errors
641
655
  @attribute = attribute
@@ -657,7 +671,7 @@ module ActiveModel
657
671
  end
658
672
  end
659
673
 
660
- class DeprecationHandlingDetailsHash < SimpleDelegator
674
+ class DeprecationHandlingDetailsHash < SimpleDelegator # :nodoc:
661
675
  def initialize(details)
662
676
  details.default = []
663
677
  details.freeze
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 3
13
- PRE = "1"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "alpha1"
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
@@ -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)
@@ -17,7 +17,7 @@ module ActiveModel
17
17
  #
18
18
  # This also provides the required class methods for hooking into the
19
19
  # Rails internationalization API, including being able to define a
20
- # class based +i18n_scope+ and +lookup_ancestors+ to find translations in
20
+ # class-based +i18n_scope+ and +lookup_ancestors+ to find translations in
21
21
  # parent classes.
22
22
  module Translation
23
23
  include ActiveModel::Naming
@@ -25,10 +25,18 @@ module ActiveModel
25
25
  end
26
26
 
27
27
  def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
28
- super || number_to_non_number?(old_value, new_value_before_type_cast)
28
+ (super || number_to_non_number?(old_value, new_value_before_type_cast)) &&
29
+ !equal_nan?(old_value, new_value_before_type_cast)
29
30
  end
30
31
 
31
32
  private
33
+ def equal_nan?(old_value, new_value)
34
+ (old_value.is_a?(::Float) || old_value.is_a?(BigDecimal)) &&
35
+ old_value.nan? &&
36
+ old_value.instance_of?(new_value.class) &&
37
+ new_value.nan?
38
+ end
39
+
32
40
  def number_to_non_number?(old_value, new_value_before_type_cast)
33
41
  old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
34
42
  end