activemodel 8.0.3 → 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 -116
- 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 +1 -1
- 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/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 +2 -0
- 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 +5 -19
- 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,129 +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
|
-
|
5
|
+
*Ben Sheldon*
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
## Rails 8.0.2.1 (August 13, 2025) ##
|
11
|
-
|
12
|
-
* No changes.
|
13
|
-
|
14
|
-
|
15
|
-
## Rails 8.0.2 (March 12, 2025) ##
|
16
|
-
|
17
|
-
* No changes.
|
18
|
-
|
19
|
-
|
20
|
-
## Rails 8.0.1 (December 13, 2024) ##
|
21
|
-
|
22
|
-
* No changes.
|
23
|
-
|
24
|
-
|
25
|
-
## Rails 8.0.0.1 (December 10, 2024) ##
|
26
|
-
|
27
|
-
* No changes.
|
28
|
-
|
29
|
-
|
30
|
-
## Rails 8.0.0 (November 07, 2024) ##
|
31
|
-
|
32
|
-
* No changes.
|
33
|
-
|
34
|
-
|
35
|
-
## Rails 8.0.0.rc2 (October 30, 2024) ##
|
36
|
-
|
37
|
-
* No changes.
|
38
|
-
|
39
|
-
|
40
|
-
## Rails 8.0.0.rc1 (October 19, 2024) ##
|
41
|
-
|
42
|
-
* Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
|
43
|
-
|
44
|
-
```ruby
|
45
|
-
class User < ApplicationRecord
|
46
|
-
#...
|
47
|
-
validates :birthday, presence: { except_on: :admin }
|
48
|
-
#...
|
49
|
-
end
|
50
|
-
|
51
|
-
user = User.new(attributes except birthday)
|
52
|
-
user.save(context: :admin)
|
53
|
-
```
|
54
|
-
|
55
|
-
*Drew Bragg*
|
56
|
-
|
57
|
-
## Rails 8.0.0.beta1 (September 26, 2024) ##
|
58
|
-
|
59
|
-
* Make `ActiveModel::Serialization#read_attribute_for_serialization` public
|
60
|
-
|
61
|
-
*Sean Doyle*
|
62
|
-
|
63
|
-
* Add a default token generator for password reset tokens when using `has_secure_password`.
|
64
|
-
|
65
|
-
```ruby
|
66
|
-
class User < ApplicationRecord
|
67
|
-
has_secure_password
|
68
|
-
end
|
69
|
-
|
70
|
-
user = User.create!(name: "david", password: "123", password_confirmation: "123")
|
71
|
-
token = user.password_reset_token
|
72
|
-
User.find_by_password_reset_token(token) # returns user
|
73
|
-
|
74
|
-
# 16 minutes later...
|
75
|
-
User.find_by_password_reset_token(token) # returns nil
|
76
|
-
|
77
|
-
# raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
|
78
|
-
User.find_by_password_reset_token!(token)
|
79
|
-
```
|
80
|
-
|
81
|
-
*DHH*
|
82
|
-
|
83
|
-
* Add a load hook `active_model_translation` for `ActiveModel::Translation`.
|
84
|
-
|
85
|
-
*Shouichi Kamiya*
|
86
|
-
|
87
|
-
* Add `raise_on_missing_translations` option to `ActiveModel::Translation`.
|
88
|
-
When the option is set, `human_attribute_name` raises an error if a translation of the given attribute is missing.
|
89
|
-
|
90
|
-
```ruby
|
91
|
-
# ActiveModel::Translation.raise_on_missing_translations = false
|
92
|
-
Post.human_attribute_name("title")
|
93
|
-
=> "Title"
|
94
|
-
|
95
|
-
# ActiveModel::Translation.raise_on_missing_translations = true
|
96
|
-
Post.human_attribute_name("title")
|
97
|
-
=> Translation missing. Options considered were: (I18n::MissingTranslationData)
|
98
|
-
- en.activerecord.attributes.post.title
|
99
|
-
- en.attributes.title
|
100
|
-
|
101
|
-
raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
|
102
|
-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
103
|
-
```
|
104
|
-
|
105
|
-
*Shouichi Kamiya*
|
106
|
-
|
107
|
-
* Introduce `ActiveModel::AttributeAssignment#attribute_writer_missing`
|
108
|
-
|
109
|
-
Provide instances with an opportunity to gracefully handle assigning to an
|
110
|
-
unknown attribute:
|
7
|
+
* Backport `ActiveRecord::Normalization` to `ActiveModel::Attributes::Normalization`
|
111
8
|
|
112
9
|
```ruby
|
113
|
-
class
|
114
|
-
include ActiveModel::
|
10
|
+
class User
|
11
|
+
include ActiveModel::Attributes
|
12
|
+
include ActiveModel::Attributes::Normalization
|
115
13
|
|
116
|
-
|
14
|
+
attribute :email, :string
|
117
15
|
|
118
|
-
|
119
|
-
Rails.logger.warn "Tried to assign to unknown attribute #{name}"
|
120
|
-
end
|
16
|
+
normalizes :email, with: -> email { email.strip.downcase }
|
121
17
|
end
|
122
18
|
|
123
|
-
|
124
|
-
|
19
|
+
user = User.new
|
20
|
+
user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
|
21
|
+
user.email # => "cruise-control@example.com"
|
125
22
|
```
|
126
23
|
|
127
24
|
*Sean Doyle*
|
128
25
|
|
129
|
-
Please check [
|
26
|
+
Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activemodel/CHANGELOG.md) for previous changes.
|
@@ -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
@@ -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
|
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/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
|
@@ -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
|
|
@@ -147,14 +147,14 @@ module ActiveModel
|
|
147
147
|
# or an array of symbols. (e.g. <tt>except: :create</tt> or
|
148
148
|
# <tt>except_on: :custom_validation_context</tt> or
|
149
149
|
# <tt>except_on: [:create, :custom_validation_context]</tt>)
|
150
|
-
# * <tt>:if</tt> - Specifies a method or
|
150
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
151
151
|
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
152
|
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method
|
153
|
-
# proc should return or evaluate to a +true+ or +false+ value.
|
154
|
-
# * <tt>:unless</tt> - Specifies a method or
|
152
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
153
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
154
|
+
# * <tt>:unless</tt> - Specifies a method, proc, or string to call to
|
155
155
|
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
156
156
|
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
157
|
-
# method or
|
157
|
+
# method, proc, or string should return or evaluate to a +true+ or +false+
|
158
158
|
# value.
|
159
159
|
#
|
160
160
|
# NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
|
@@ -439,20 +439,6 @@ module ActiveModel
|
|
439
439
|
alias :read_attribute_for_validation :send
|
440
440
|
|
441
441
|
# Returns the context when running validations.
|
442
|
-
#
|
443
|
-
# This is useful when running validations except a certain context (opposite to the +on+ option).
|
444
|
-
#
|
445
|
-
# class Person
|
446
|
-
# include ActiveModel::Validations
|
447
|
-
#
|
448
|
-
# attr_accessor :name
|
449
|
-
# validates :name, presence: true, if: -> { validation_context != :custom }
|
450
|
-
# end
|
451
|
-
#
|
452
|
-
# person = Person.new
|
453
|
-
# person.valid? #=> false
|
454
|
-
# person.valid?(:new) #=> false
|
455
|
-
# person.valid?(:custom) #=> true
|
456
442
|
def validation_context
|
457
443
|
context_for_validation.context
|
458
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:
|