activemodel 8.0.2.1 → 8.1.0.beta1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -112
- data/README.rdoc +1 -1
- data/lib/active_model/attribute/user_provided_default.rb +10 -0
- data/lib/active_model/attribute.rb +9 -1
- data/lib/active_model/attribute_methods.rb +2 -2
- data/lib/active_model/attribute_mutation_tracker.rb +5 -5
- data/lib/active_model/attribute_set.rb +1 -1
- data/lib/active_model/attributes/normalization.rb +192 -0
- data/lib/active_model/dirty.rb +6 -6
- data/lib/active_model/error.rb +3 -2
- data/lib/active_model/errors.rb +1 -4
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +7 -3
- data/lib/active_model/model.rb +2 -2
- data/lib/active_model/naming.rb +0 -1
- data/lib/active_model/nested_error.rb +1 -3
- data/lib/active_model/railtie.rb +7 -3
- data/lib/active_model/secure_password.rb +3 -1
- data/lib/active_model/type/big_integer.rb +21 -0
- data/lib/active_model/type/boolean.rb +1 -0
- data/lib/active_model/type/date.rb +1 -0
- data/lib/active_model/type/date_time.rb +8 -0
- data/lib/active_model/type/decimal.rb +1 -0
- data/lib/active_model/type/float.rb +1 -0
- data/lib/active_model/type/helpers/immutable.rb +13 -0
- data/lib/active_model/type/helpers.rb +1 -0
- data/lib/active_model/type/immutable_string.rb +2 -0
- data/lib/active_model/type/integer.rb +31 -19
- data/lib/active_model/type/string.rb +4 -0
- data/lib/active_model/type/value.rb +2 -2
- data/lib/active_model/validations/callbacks.rb +10 -0
- data/lib/active_model/validations.rb +4 -16
- data/lib/active_model.rb +6 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0477968c49c9249ac55371d34c8035ada03438da3b79f12d53f29179913018a
|
4
|
+
data.tar.gz: e9481d73df257802b3b69b46837b9806417f2244ecb672c397a2dc9944528799
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 652d349baa6d7d2354878e7e70300036f6529ca8a0e5ef700eca8ed58e936b3d2fa18f293dc69aba2ba3ed7c54bec30631b23070969f7e9e57c18e663f289e15
|
7
|
+
data.tar.gz: c188746810b0ce0b3645cbc32d288c16864eed5c1ce1c1569a5f6d0aa93b4d761e68bfacb77956be90a3de7449f60d6467813c74e3115a5baddd618190013ea9
|
data/CHANGELOG.md
CHANGED
@@ -1,125 +1,26 @@
|
|
1
|
-
## Rails 8.0.
|
1
|
+
## Rails 8.1.0.beta1 (September 04, 2025) ##
|
2
2
|
|
3
|
-
*
|
3
|
+
* Add `except_on:` option for validation callbacks.
|
4
4
|
|
5
|
+
*Ben Sheldon*
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
* No changes.
|
9
|
-
|
10
|
-
|
11
|
-
## Rails 8.0.2 (March 12, 2025) ##
|
12
|
-
|
13
|
-
* No changes.
|
14
|
-
|
15
|
-
|
16
|
-
## Rails 8.0.1 (December 13, 2024) ##
|
17
|
-
|
18
|
-
* No changes.
|
19
|
-
|
20
|
-
|
21
|
-
## Rails 8.0.0.1 (December 10, 2024) ##
|
22
|
-
|
23
|
-
* No changes.
|
24
|
-
|
25
|
-
|
26
|
-
## Rails 8.0.0 (November 07, 2024) ##
|
27
|
-
|
28
|
-
* No changes.
|
29
|
-
|
30
|
-
|
31
|
-
## Rails 8.0.0.rc2 (October 30, 2024) ##
|
32
|
-
|
33
|
-
* No changes.
|
34
|
-
|
35
|
-
|
36
|
-
## Rails 8.0.0.rc1 (October 19, 2024) ##
|
37
|
-
|
38
|
-
* Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
|
39
|
-
|
40
|
-
```ruby
|
41
|
-
class User < ApplicationRecord
|
42
|
-
#...
|
43
|
-
validates :birthday, presence: { except_on: :admin }
|
44
|
-
#...
|
45
|
-
end
|
46
|
-
|
47
|
-
user = User.new(attributes except birthday)
|
48
|
-
user.save(context: :admin)
|
49
|
-
```
|
50
|
-
|
51
|
-
*Drew Bragg*
|
52
|
-
|
53
|
-
## Rails 8.0.0.beta1 (September 26, 2024) ##
|
54
|
-
|
55
|
-
* Make `ActiveModel::Serialization#read_attribute_for_serialization` public
|
56
|
-
|
57
|
-
*Sean Doyle*
|
58
|
-
|
59
|
-
* Add a default token generator for password reset tokens when using `has_secure_password`.
|
60
|
-
|
61
|
-
```ruby
|
62
|
-
class User < ApplicationRecord
|
63
|
-
has_secure_password
|
64
|
-
end
|
65
|
-
|
66
|
-
user = User.create!(name: "david", password: "123", password_confirmation: "123")
|
67
|
-
token = user.password_reset_token
|
68
|
-
User.find_by_password_reset_token(token) # returns user
|
69
|
-
|
70
|
-
# 16 minutes later...
|
71
|
-
User.find_by_password_reset_token(token) # returns nil
|
72
|
-
|
73
|
-
# raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
|
74
|
-
User.find_by_password_reset_token!(token)
|
75
|
-
```
|
76
|
-
|
77
|
-
*DHH*
|
78
|
-
|
79
|
-
* Add a load hook `active_model_translation` for `ActiveModel::Translation`.
|
80
|
-
|
81
|
-
*Shouichi Kamiya*
|
82
|
-
|
83
|
-
* Add `raise_on_missing_translations` option to `ActiveModel::Translation`.
|
84
|
-
When the option is set, `human_attribute_name` raises an error if a translation of the given attribute is missing.
|
85
|
-
|
86
|
-
```ruby
|
87
|
-
# ActiveModel::Translation.raise_on_missing_translations = false
|
88
|
-
Post.human_attribute_name("title")
|
89
|
-
=> "Title"
|
90
|
-
|
91
|
-
# ActiveModel::Translation.raise_on_missing_translations = true
|
92
|
-
Post.human_attribute_name("title")
|
93
|
-
=> Translation missing. Options considered were: (I18n::MissingTranslationData)
|
94
|
-
- en.activerecord.attributes.post.title
|
95
|
-
- en.attributes.title
|
96
|
-
|
97
|
-
raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
|
98
|
-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
99
|
-
```
|
100
|
-
|
101
|
-
*Shouichi Kamiya*
|
102
|
-
|
103
|
-
* Introduce `ActiveModel::AttributeAssignment#attribute_writer_missing`
|
104
|
-
|
105
|
-
Provide instances with an opportunity to gracefully handle assigning to an
|
106
|
-
unknown attribute:
|
7
|
+
* Backport `ActiveRecord::Normalization` to `ActiveModel::Attributes::Normalization`
|
107
8
|
|
108
9
|
```ruby
|
109
|
-
class
|
110
|
-
include ActiveModel::
|
10
|
+
class User
|
11
|
+
include ActiveModel::Attributes
|
12
|
+
include ActiveModel::Attributes::Normalization
|
111
13
|
|
112
|
-
|
14
|
+
attribute :email, :string
|
113
15
|
|
114
|
-
|
115
|
-
Rails.logger.warn "Tried to assign to unknown attribute #{name}"
|
116
|
-
end
|
16
|
+
normalizes :email, with: -> email { email.strip.downcase }
|
117
17
|
end
|
118
18
|
|
119
|
-
|
120
|
-
|
19
|
+
user = User.new
|
20
|
+
user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
|
21
|
+
user.email # => "cruise-control@example.com"
|
121
22
|
```
|
122
23
|
|
123
24
|
*Sean Doyle*
|
124
25
|
|
125
|
-
Please check [
|
26
|
+
Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activemodel/CHANGELOG.md) for previous changes.
|
data/README.rdoc
CHANGED
@@ -261,6 +261,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
|
|
261
261
|
|
262
262
|
* https://github.com/rails/rails/issues
|
263
263
|
|
264
|
-
Feature requests should be discussed on the
|
264
|
+
Feature requests should be discussed on the rubyonrails-core forum here:
|
265
265
|
|
266
266
|
* https://discuss.rubyonrails.org/c/rubyonrails-core
|
@@ -26,6 +26,16 @@ module ActiveModel
|
|
26
26
|
self.class.new(name, user_provided_value, type, original_attribute)
|
27
27
|
end
|
28
28
|
|
29
|
+
def dup_or_share # :nodoc:
|
30
|
+
# Can't elide dup when the default is a Proc
|
31
|
+
# See Attribute#dup_or_share
|
32
|
+
if @user_provided_value.is_a?(Proc)
|
33
|
+
dup
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
29
39
|
def marshal_dump
|
30
40
|
result = [
|
31
41
|
name,
|
@@ -38,7 +38,7 @@ module ActiveModel
|
|
38
38
|
@value = value unless value.nil?
|
39
39
|
end
|
40
40
|
|
41
|
-
def value(&
|
41
|
+
def value(&)
|
42
42
|
# `defined?` is cheaper than `||=` when we get back falsy values
|
43
43
|
@value = type_cast(value_before_type_cast) unless defined?(@value)
|
44
44
|
@value
|
@@ -96,6 +96,14 @@ module ActiveModel
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
+
def dup_or_share # :nodoc:
|
100
|
+
if @type.mutable?
|
101
|
+
dup
|
102
|
+
else
|
103
|
+
self # If the underlying type is immutable we can get away with not duping
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
99
107
|
def type_cast(*)
|
100
108
|
raise NotImplementedError
|
101
109
|
end
|
@@ -373,7 +373,7 @@ module ActiveModel
|
|
373
373
|
# person.name_short? # => NoMethodError
|
374
374
|
# person.first_name # => NoMethodError
|
375
375
|
def undefine_attribute_methods
|
376
|
-
generated_attribute_methods
|
376
|
+
@generated_attribute_methods&.module_eval do
|
377
377
|
undef_method(*instance_methods)
|
378
378
|
end
|
379
379
|
attribute_method_patterns_cache.clear
|
@@ -402,7 +402,7 @@ module ActiveModel
|
|
402
402
|
end
|
403
403
|
|
404
404
|
def instance_method_already_implemented?(method_name)
|
405
|
-
generated_attribute_methods
|
405
|
+
@generated_attribute_methods&.method_defined?(method_name)
|
406
406
|
end
|
407
407
|
|
408
408
|
# The methods +method_missing+ and +respond_to?+ of this module are
|
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/duplicable"
|
|
5
5
|
|
6
6
|
module ActiveModel
|
7
7
|
class AttributeMutationTracker # :nodoc:
|
8
|
-
OPTION_NOT_GIVEN = Object.new
|
8
|
+
OPTION_NOT_GIVEN = Object.new.freeze
|
9
9
|
|
10
10
|
def initialize(attributes)
|
11
11
|
@attributes = attributes
|
@@ -16,17 +16,17 @@ module ActiveModel
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def changed_values
|
19
|
-
attr_names.each_with_object(
|
19
|
+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
|
20
20
|
if changed?(attr_name)
|
21
|
-
result
|
21
|
+
result.store(attr_name, original_value(attr_name), convert_value: false)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
def changes
|
27
|
-
attr_names.each_with_object(
|
27
|
+
attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
|
28
28
|
if change = change_to_attribute(attr_name)
|
29
|
-
result.
|
29
|
+
result.store(attr_name, change, convert_value: false)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Attributes
|
5
|
+
module Normalization
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ActiveModel::Dirty
|
10
|
+
include ActiveModel::Validations::Callbacks
|
11
|
+
|
12
|
+
class_attribute :normalized_attributes, default: Set.new
|
13
|
+
|
14
|
+
before_validation :normalize_changed_in_place_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# Normalizes a specified attribute using its declared normalizations.
|
18
|
+
#
|
19
|
+
# ==== Examples
|
20
|
+
#
|
21
|
+
# class User
|
22
|
+
# include ActiveModel::Attributes
|
23
|
+
# include ActiveModel::Attributes::Normalization
|
24
|
+
#
|
25
|
+
# attribute :email, :string
|
26
|
+
#
|
27
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# legacy_user = User.load_from_legacy_data(...)
|
31
|
+
# legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
|
32
|
+
# legacy_user.normalize_attribute(:email)
|
33
|
+
# legacy_user.email # => "cruise-control@example.com"
|
34
|
+
#
|
35
|
+
# ==== Behavior with Active Record
|
36
|
+
#
|
37
|
+
# To prevent confusion, normalization will not be applied
|
38
|
+
# when the attribute is fetched from the database. This means that if a
|
39
|
+
# record was persisted before the normalization was declared, the record's
|
40
|
+
# attribute will not be normalized until either it is assigned a new
|
41
|
+
# value, or it is explicitly migrated via Normalization#normalize_attribute.
|
42
|
+
#
|
43
|
+
# Be aware that if your app was created before Rails 7.1, and your app
|
44
|
+
# marshals instances of the targeted model (for example, when caching),
|
45
|
+
# then you should set ActiveRecord.marshalling_format_version to +7.1+ or
|
46
|
+
# higher via either <tt>config.load_defaults 7.1</tt> or
|
47
|
+
# <tt>config.active_record.marshalling_format_version = 7.1</tt>.
|
48
|
+
# Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
|
49
|
+
# and raise +TypeError+.
|
50
|
+
#
|
51
|
+
# class User < ActiveRecord::Base
|
52
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
53
|
+
# normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
|
57
|
+
# user.email # => "cruise-control@example.com"
|
58
|
+
#
|
59
|
+
# user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
|
60
|
+
# user.email # => "cruise-control@example.com"
|
61
|
+
# user.email_before_type_cast # => "cruise-control@example.com"
|
62
|
+
#
|
63
|
+
# User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
|
64
|
+
# User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
|
65
|
+
#
|
66
|
+
# User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
|
67
|
+
# User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
|
68
|
+
#
|
69
|
+
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
|
70
|
+
def normalize_attribute(name)
|
71
|
+
# Treat the value as a new, unnormalized value.
|
72
|
+
send(:"#{name}=", send(name))
|
73
|
+
end
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
# Declares a normalization for one or more attributes. The normalization
|
77
|
+
# is applied when the attribute is assigned or validated.
|
78
|
+
#
|
79
|
+
# Because the normalization may be applied multiple times, it should be
|
80
|
+
# _idempotent_. In other words, applying the normalization more than once
|
81
|
+
# should have the same result as applying it only once.
|
82
|
+
#
|
83
|
+
# By default, the normalization will not be applied to +nil+ values. This
|
84
|
+
# behavior can be changed with the +:apply_to_nil+ option.
|
85
|
+
#
|
86
|
+
# ==== Options
|
87
|
+
#
|
88
|
+
# * +:with+ - Any callable object that accepts the attribute's value as
|
89
|
+
# its sole argument, and returns it normalized.
|
90
|
+
# * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
|
91
|
+
# Defaults to +false+.
|
92
|
+
#
|
93
|
+
# ==== Examples
|
94
|
+
#
|
95
|
+
# class User
|
96
|
+
# include ActiveModel::Attributes
|
97
|
+
# include ActiveModel::Attributes::Normalization
|
98
|
+
#
|
99
|
+
# attribute :email, :string
|
100
|
+
# attribute :phone, :string
|
101
|
+
#
|
102
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
103
|
+
# normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# user = User.new
|
107
|
+
# user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
|
108
|
+
# user.email # => "cruise-control@example.com"
|
109
|
+
#
|
110
|
+
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
|
111
|
+
def normalizes(*names, with:, apply_to_nil: false)
|
112
|
+
decorate_attributes(names) do |name, cast_type|
|
113
|
+
NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
|
114
|
+
end
|
115
|
+
|
116
|
+
self.normalized_attributes += names.map(&:to_sym)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Normalizes a given +value+ using normalizations declared for +name+.
|
120
|
+
#
|
121
|
+
# ==== Examples
|
122
|
+
#
|
123
|
+
# class User
|
124
|
+
# include ActiveModel::Attributes
|
125
|
+
# include ActiveModel::Attributes::Normalization
|
126
|
+
#
|
127
|
+
# attribute :email, :string
|
128
|
+
#
|
129
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
|
133
|
+
# # => "cruise-control@example.com"
|
134
|
+
def normalize_value_for(name, value)
|
135
|
+
type_for_attribute(name).cast(value)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def normalize_changed_in_place_attributes
|
141
|
+
self.class.normalized_attributes.each do |name|
|
142
|
+
normalize_attribute(name) if attribute_changed_in_place?(name)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
|
147
|
+
include ActiveModel::Type::SerializeCastValue
|
148
|
+
|
149
|
+
attr_reader :cast_type, :normalizer, :normalize_nil
|
150
|
+
alias :normalize_nil? :normalize_nil
|
151
|
+
|
152
|
+
def initialize(cast_type:, normalizer:, normalize_nil:)
|
153
|
+
@cast_type = cast_type
|
154
|
+
@normalizer = normalizer
|
155
|
+
@normalize_nil = normalize_nil
|
156
|
+
super(cast_type)
|
157
|
+
end
|
158
|
+
|
159
|
+
def cast(value)
|
160
|
+
normalize(super(value))
|
161
|
+
end
|
162
|
+
|
163
|
+
def serialize(value)
|
164
|
+
serialize_cast_value(cast(value))
|
165
|
+
end
|
166
|
+
|
167
|
+
def serialize_cast_value(value)
|
168
|
+
ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
|
169
|
+
end
|
170
|
+
|
171
|
+
def ==(other)
|
172
|
+
self.class == other.class &&
|
173
|
+
normalize_nil? == other.normalize_nil? &&
|
174
|
+
normalizer == other.normalizer &&
|
175
|
+
cast_type == other.cast_type
|
176
|
+
end
|
177
|
+
alias eql? ==
|
178
|
+
|
179
|
+
def hash
|
180
|
+
[self.class, cast_type, normalizer, normalize_nil?].hash
|
181
|
+
end
|
182
|
+
|
183
|
+
define_method(:inspect, Kernel.instance_method(:inspect))
|
184
|
+
|
185
|
+
private
|
186
|
+
def normalize(value)
|
187
|
+
normalizer.call(value) unless value.nil? && !normalize_nil?
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/active_model/dirty.rb
CHANGED
@@ -108,7 +108,7 @@ module ActiveModel
|
|
108
108
|
# person.changes # => {"name" => ["Bill", "Bob"]}
|
109
109
|
#
|
110
110
|
# If an attribute is modified in-place then make use of
|
111
|
-
# {*_will_change!}[rdoc-
|
111
|
+
# {*_will_change!}[rdoc-ref:#*_will_change!] to mark that the attribute is changing.
|
112
112
|
# Otherwise \Active \Model can't track changes to in-place attributes. Note
|
113
113
|
# that Active Record can detect in-place modifications automatically. You do
|
114
114
|
# not need to call <tt>*_will_change!</tt> on Active Record models.
|
@@ -252,7 +252,7 @@ module ActiveModel
|
|
252
252
|
|
253
253
|
def init_attributes(other) # :nodoc:
|
254
254
|
attrs = super
|
255
|
-
if
|
255
|
+
if self.class.respond_to?(:_default_attributes)
|
256
256
|
self.class._default_attributes.map do |attr|
|
257
257
|
attr.with_value_from_user(attrs.fetch_value(attr.name))
|
258
258
|
end
|
@@ -296,22 +296,22 @@ module ActiveModel
|
|
296
296
|
mutations_from_database.changed_attribute_names
|
297
297
|
end
|
298
298
|
|
299
|
-
# Dispatch target for {*_changed?}[rdoc-
|
299
|
+
# Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
|
300
300
|
def attribute_changed?(attr_name, **options)
|
301
301
|
mutations_from_database.changed?(attr_name.to_s, **options)
|
302
302
|
end
|
303
303
|
|
304
|
-
# Dispatch target for {*_was}[rdoc-
|
304
|
+
# Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
|
305
305
|
def attribute_was(attr_name)
|
306
306
|
mutations_from_database.original_value(attr_name.to_s)
|
307
307
|
end
|
308
308
|
|
309
|
-
# Dispatch target for {*_previously_changed?}[rdoc-
|
309
|
+
# Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
|
310
310
|
def attribute_previously_changed?(attr_name, **options)
|
311
311
|
mutations_before_last_save.changed?(attr_name.to_s, **options)
|
312
312
|
end
|
313
313
|
|
314
|
-
# Dispatch target for {*_previously_was}[rdoc-
|
314
|
+
# Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
|
315
315
|
def attribute_previously_was(attr_name)
|
316
316
|
mutations_before_last_save.original_value(attr_name.to_s)
|
317
317
|
end
|
data/lib/active_model/error.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/core_ext/class/attribute"
|
4
3
|
|
5
4
|
module ActiveModel
|
6
5
|
# = Active \Model \Error
|
@@ -83,7 +82,7 @@ module ActiveModel
|
|
83
82
|
defaults << :"#{i18n_scope}.errors.messages.#{type}"
|
84
83
|
|
85
84
|
catch(:exception) do
|
86
|
-
translation = I18n.translate(defaults.first, **options
|
85
|
+
translation = I18n.translate(defaults.first, **options, default: defaults.drop(1), throw: true)
|
87
86
|
return translation unless translation.nil?
|
88
87
|
end unless options[:message]
|
89
88
|
else
|
@@ -205,4 +204,6 @@ module ActiveModel
|
|
205
204
|
[@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
|
206
205
|
end
|
207
206
|
end
|
207
|
+
|
208
|
+
ActiveSupport.run_load_hooks(:active_model_error, Error)
|
208
209
|
end
|
data/lib/active_model/errors.rb
CHANGED
@@ -5,7 +5,6 @@ require "active_support/core_ext/string/inflections"
|
|
5
5
|
require "active_support/core_ext/object/deep_dup"
|
6
6
|
require "active_model/error"
|
7
7
|
require "active_model/nested_error"
|
8
|
-
require "forwardable"
|
9
8
|
|
10
9
|
module ActiveModel
|
11
10
|
# = Active \Model \Errors
|
@@ -61,8 +60,6 @@ module ActiveModel
|
|
61
60
|
class Errors
|
62
61
|
include Enumerable
|
63
62
|
|
64
|
-
extend Forwardable
|
65
|
-
|
66
63
|
##
|
67
64
|
# :method: each
|
68
65
|
#
|
@@ -100,7 +97,7 @@ module ActiveModel
|
|
100
97
|
#
|
101
98
|
# Returns number of errors.
|
102
99
|
|
103
|
-
|
100
|
+
delegate :each, :clear, :empty?, :size, :uniq!, to: :@errors
|
104
101
|
|
105
102
|
# The actual array of +Error+ objects
|
106
103
|
# This method is aliased to <tt>objects</tt>.
|
data/lib/active_model/lint.rb
CHANGED
@@ -30,7 +30,7 @@ module ActiveModel
|
|
30
30
|
# of the model, and is used to a generate unique DOM id for the object.
|
31
31
|
def test_to_key
|
32
32
|
assert_respond_to model, :to_key
|
33
|
-
|
33
|
+
def_method(model, :persisted?) { false }
|
34
34
|
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
|
35
35
|
end
|
36
36
|
|
@@ -45,8 +45,8 @@ module ActiveModel
|
|
45
45
|
# any of the possible implementation strategies on the implementer.
|
46
46
|
def test_to_param
|
47
47
|
assert_respond_to model, :to_param
|
48
|
-
|
49
|
-
|
48
|
+
def_method(model, :to_key) { [1] }
|
49
|
+
def_method(model, :persisted?) { false }
|
50
50
|
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
|
51
51
|
end
|
52
52
|
|
@@ -105,6 +105,10 @@ module ActiveModel
|
|
105
105
|
end
|
106
106
|
|
107
107
|
private
|
108
|
+
def def_method(receiver, name, &block)
|
109
|
+
::Object.instance_method(:define_singleton_method).bind_call(receiver, name, &block)
|
110
|
+
end
|
111
|
+
|
108
112
|
def model
|
109
113
|
assert_respond_to @model, :to_model
|
110
114
|
@model.to_model
|
data/lib/active_model/model.rb
CHANGED
@@ -54,7 +54,7 @@ module ActiveModel
|
|
54
54
|
#
|
55
55
|
# person = Person.new(id: 1, name: "bob")
|
56
56
|
# person.slice(:id, :name)
|
57
|
-
# => { "id" => 1, "name" => "bob" }
|
57
|
+
# # => { "id" => 1, "name" => "bob" }
|
58
58
|
#
|
59
59
|
#--
|
60
60
|
# Implemented by ActiveModel::Access#slice.
|
@@ -68,7 +68,7 @@ module ActiveModel
|
|
68
68
|
#
|
69
69
|
# person = Person.new(id: 1, name: "bob")
|
70
70
|
# person.values_at(:id, :name)
|
71
|
-
# => [1, "bob"]
|
71
|
+
# # => [1, "bob"]
|
72
72
|
#
|
73
73
|
#--
|
74
74
|
# Implemented by ActiveModel::Access#values_at.
|
data/lib/active_model/naming.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_model/error"
|
4
|
-
require "forwardable"
|
5
4
|
|
6
5
|
module ActiveModel
|
7
6
|
class NestedError < Error
|
@@ -16,7 +15,6 @@ module ActiveModel
|
|
16
15
|
|
17
16
|
attr_reader :inner_error
|
18
17
|
|
19
|
-
|
20
|
-
def_delegators :@inner_error, :message
|
18
|
+
delegate :message, to: :@inner_error
|
21
19
|
end
|
22
20
|
end
|
data/lib/active_model/railtie.rb
CHANGED
@@ -14,11 +14,15 @@ module ActiveModel
|
|
14
14
|
end
|
15
15
|
|
16
16
|
initializer "active_model.secure_password" do
|
17
|
-
|
17
|
+
ActiveSupport.on_load(:active_model_secure_password) do
|
18
|
+
ActiveModel::SecurePassword.min_cost = Rails.env.test?
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
|
-
initializer "active_model.i18n_customize_full_message" do
|
21
|
-
|
22
|
+
initializer "active_model.i18n_customize_full_message" do |app|
|
23
|
+
ActiveSupport.on_load(:active_model_error) do
|
24
|
+
ActiveModel::Error.i18n_customize_full_message = app.config.active_model.i18n_customize_full_message || false
|
25
|
+
end
|
22
26
|
end
|
23
27
|
end
|
24
28
|
end
|
@@ -155,7 +155,7 @@ module ActiveModel
|
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
158
|
-
validates_confirmation_of attribute,
|
158
|
+
validates_confirmation_of attribute, allow_nil: true
|
159
159
|
end
|
160
160
|
|
161
161
|
# Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
|
@@ -228,4 +228,6 @@ module ActiveModel
|
|
228
228
|
end
|
229
229
|
end
|
230
230
|
end
|
231
|
+
|
232
|
+
ActiveSupport.run_load_hooks(:active_model_secure_password, SecurePassword)
|
231
233
|
end
|
@@ -23,10 +23,31 @@ module ActiveModel
|
|
23
23
|
# All casting and serialization are performed in the same way as the
|
24
24
|
# standard ActiveModel::Type::Integer type.
|
25
25
|
class BigInteger < Integer
|
26
|
+
def serialize(value) # :nodoc:
|
27
|
+
case value
|
28
|
+
when ::Integer
|
29
|
+
# noop
|
30
|
+
when ::String
|
31
|
+
int = value.to_i
|
32
|
+
if int.zero? && value != "0"
|
33
|
+
return if non_numeric_string?(value)
|
34
|
+
end
|
35
|
+
value = int
|
36
|
+
else
|
37
|
+
value = super
|
38
|
+
end
|
39
|
+
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
26
43
|
def serialize_cast_value(value) # :nodoc:
|
27
44
|
value
|
28
45
|
end
|
29
46
|
|
47
|
+
def serializable?(value, &)
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
30
51
|
private
|
31
52
|
def max_value
|
32
53
|
::Float::INFINITY
|
@@ -24,6 +24,7 @@ module ActiveModel
|
|
24
24
|
# String values are parsed using the ISO 8601 date format. Any other values
|
25
25
|
# are cast using their +to_date+ method, if it exists.
|
26
26
|
class Date < Value
|
27
|
+
include Helpers::Immutable
|
27
28
|
include Helpers::Timezone
|
28
29
|
include Helpers::AcceptsMultiparameterTime.new
|
29
30
|
|
@@ -50,6 +50,14 @@ module ActiveModel
|
|
50
50
|
:datetime
|
51
51
|
end
|
52
52
|
|
53
|
+
def mutable? # :nodoc:
|
54
|
+
# Time#zone can be mutated by #utc or #localtime
|
55
|
+
# However when serializing the time zone will always
|
56
|
+
# be coerced and even if the zone was mutated Time instances
|
57
|
+
# remain equal, so we don't need to implement `#changed_in_place?`
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
53
61
|
private
|
54
62
|
def cast_value(value)
|
55
63
|
return apply_seconds_precision(value) unless value.is_a?(::String)
|
@@ -3,5 +3,6 @@
|
|
3
3
|
require "active_model/type/helpers/accepts_multiparameter_time"
|
4
4
|
require "active_model/type/helpers/numeric"
|
5
5
|
require "active_model/type/helpers/mutable"
|
6
|
+
require "active_model/type/helpers/immutable"
|
6
7
|
require "active_model/type/helpers/time_value"
|
7
8
|
require "active_model/type/helpers/timezone"
|
@@ -42,6 +42,7 @@ module ActiveModel
|
|
42
42
|
# attribute :age, :integer, limit: 6
|
43
43
|
# end
|
44
44
|
class Integer < Value
|
45
|
+
include Helpers::Immutable
|
45
46
|
include Helpers::Numeric
|
46
47
|
|
47
48
|
# Column storage size in bytes.
|
@@ -50,7 +51,8 @@ module ActiveModel
|
|
50
51
|
|
51
52
|
def initialize(**)
|
52
53
|
super
|
53
|
-
@
|
54
|
+
@max = max_value
|
55
|
+
@min = min_value
|
54
56
|
end
|
55
57
|
|
56
58
|
def type
|
@@ -63,40 +65,50 @@ module ActiveModel
|
|
63
65
|
end
|
64
66
|
|
65
67
|
def serialize(value)
|
66
|
-
|
67
|
-
|
68
|
+
case value
|
69
|
+
when ::Integer
|
70
|
+
# noop
|
71
|
+
when ::String
|
72
|
+
int = value.to_i
|
73
|
+
if int.zero? && value != "0"
|
74
|
+
return if non_numeric_string?(value)
|
75
|
+
end
|
76
|
+
value = int
|
77
|
+
else
|
78
|
+
value = super
|
79
|
+
end
|
80
|
+
|
81
|
+
if out_of_range?(value)
|
82
|
+
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
|
83
|
+
end
|
84
|
+
|
85
|
+
value
|
68
86
|
end
|
69
87
|
|
70
88
|
def serialize_cast_value(value) # :nodoc:
|
71
|
-
|
89
|
+
if out_of_range?(value)
|
90
|
+
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
|
91
|
+
end
|
92
|
+
|
93
|
+
value
|
72
94
|
end
|
73
95
|
|
74
96
|
def serializable?(value)
|
75
97
|
cast_value = cast(value)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
98
|
+
return true unless out_of_range?(cast_value)
|
99
|
+
yield cast_value if block_given?
|
100
|
+
false
|
80
101
|
end
|
81
102
|
|
82
103
|
private
|
83
|
-
|
84
|
-
|
85
|
-
def in_range?(value)
|
86
|
-
!value || range.member?(value)
|
104
|
+
def out_of_range?(value)
|
105
|
+
value && (@max <= value || @min > value)
|
87
106
|
end
|
88
107
|
|
89
108
|
def cast_value(value)
|
90
109
|
value.to_i rescue nil
|
91
110
|
end
|
92
111
|
|
93
|
-
def ensure_in_range(value)
|
94
|
-
unless in_range?(value)
|
95
|
-
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
|
96
|
-
end
|
97
|
-
value
|
98
|
-
end
|
99
|
-
|
100
112
|
def max_value
|
101
113
|
1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
|
102
114
|
end
|
@@ -25,7 +25,7 @@ module ActiveModel
|
|
25
25
|
# by the database. For example a boolean type can return +true+ if the
|
26
26
|
# value parameter is a Ruby boolean, but may return +false+ if the value
|
27
27
|
# parameter is some other object.
|
28
|
-
def serializable?(value, &
|
28
|
+
def serializable?(value, &)
|
29
29
|
true
|
30
30
|
end
|
31
31
|
|
@@ -138,7 +138,7 @@ module ActiveModel
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def mutable? # :nodoc:
|
141
|
-
|
141
|
+
true
|
142
142
|
end
|
143
143
|
|
144
144
|
def as_json(*)
|
@@ -106,6 +106,16 @@ module ActiveModel
|
|
106
106
|
*options[:if]
|
107
107
|
]
|
108
108
|
end
|
109
|
+
|
110
|
+
if options.key?(:except_on)
|
111
|
+
options[:except_on] = Array(options[:except_on])
|
112
|
+
options[:unless] = [
|
113
|
+
->(o) {
|
114
|
+
options[:except_on].intersect?(Array(o.validation_context))
|
115
|
+
},
|
116
|
+
*options[:unless]
|
117
|
+
]
|
118
|
+
end
|
109
119
|
end
|
110
120
|
end
|
111
121
|
|
@@ -63,7 +63,8 @@ module ActiveModel
|
|
63
63
|
# end
|
64
64
|
# end
|
65
65
|
#
|
66
|
-
# Options
|
66
|
+
# ==== Options
|
67
|
+
#
|
67
68
|
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
|
68
69
|
# Runs in all validation contexts by default +nil+. You can pass a symbol
|
69
70
|
# or an array of symbols. (e.g. <tt>on: :create</tt> or
|
@@ -134,7 +135,8 @@ module ActiveModel
|
|
134
135
|
# Note that the return value of validation methods is not relevant.
|
135
136
|
# It's not possible to halt the validate callback chain.
|
136
137
|
#
|
137
|
-
# Options
|
138
|
+
# ==== Options
|
139
|
+
#
|
138
140
|
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
|
139
141
|
# Runs in all validation contexts by default +nil+. You can pass a symbol
|
140
142
|
# or an array of symbols. (e.g. <tt>on: :create</tt> or
|
@@ -437,20 +439,6 @@ module ActiveModel
|
|
437
439
|
alias :read_attribute_for_validation :send
|
438
440
|
|
439
441
|
# Returns the context when running validations.
|
440
|
-
#
|
441
|
-
# This is useful when running validations except a certain context (opposite to the +on+ option).
|
442
|
-
#
|
443
|
-
# class Person
|
444
|
-
# include ActiveModel::Validations
|
445
|
-
#
|
446
|
-
# attr_accessor :name
|
447
|
-
# validates :name, presence: true, if: -> { validation_context != :custom }
|
448
|
-
# end
|
449
|
-
#
|
450
|
-
# person = Person.new
|
451
|
-
# person.valid? #=> false
|
452
|
-
# person.valid?(:new) #=> false
|
453
|
-
# person.valid?(:custom) #=> true
|
454
442
|
def validation_context
|
455
443
|
context_for_validation.context
|
456
444
|
end
|
data/lib/active_model.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activemodel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 8.0.
|
4
|
+
version: 8.1.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
@@ -15,14 +15,14 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - '='
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 8.0.
|
18
|
+
version: 8.1.0.beta1
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - '='
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 8.0.
|
25
|
+
version: 8.1.0.beta1
|
26
26
|
description: A toolkit for building modeling frameworks like Active Record. Rich support
|
27
27
|
for attributes, callbacks, validations, serialization, internationalization, and
|
28
28
|
testing.
|
@@ -47,6 +47,7 @@ files:
|
|
47
47
|
- lib/active_model/attribute_set/builder.rb
|
48
48
|
- lib/active_model/attribute_set/yaml_encoder.rb
|
49
49
|
- lib/active_model/attributes.rb
|
50
|
+
- lib/active_model/attributes/normalization.rb
|
50
51
|
- lib/active_model/callbacks.rb
|
51
52
|
- lib/active_model/conversion.rb
|
52
53
|
- lib/active_model/deprecator.rb
|
@@ -75,6 +76,7 @@ files:
|
|
75
76
|
- lib/active_model/type/float.rb
|
76
77
|
- lib/active_model/type/helpers.rb
|
77
78
|
- lib/active_model/type/helpers/accepts_multiparameter_time.rb
|
79
|
+
- lib/active_model/type/helpers/immutable.rb
|
78
80
|
- lib/active_model/type/helpers/mutable.rb
|
79
81
|
- lib/active_model/type/helpers/numeric.rb
|
80
82
|
- lib/active_model/type/helpers/time_value.rb
|
@@ -111,10 +113,10 @@ licenses:
|
|
111
113
|
- MIT
|
112
114
|
metadata:
|
113
115
|
bug_tracker_uri: https://github.com/rails/rails/issues
|
114
|
-
changelog_uri: https://github.com/rails/rails/blob/v8.0.
|
115
|
-
documentation_uri: https://api.rubyonrails.org/v8.0.
|
116
|
+
changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/activemodel/CHANGELOG.md
|
117
|
+
documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
|
116
118
|
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
117
|
-
source_code_uri: https://github.com/rails/rails/tree/v8.0.
|
119
|
+
source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/activemodel
|
118
120
|
rubygems_mfa_required: 'true'
|
119
121
|
rdoc_options: []
|
120
122
|
require_paths:
|