activemodel 6.0.3.3 → 6.1.1
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 +49 -183
- data/MIT-LICENSE +1 -1
- data/README.rdoc +1 -1
- data/lib/active_model.rb +2 -1
- data/lib/active_model/attribute.rb +15 -14
- data/lib/active_model/attribute_assignment.rb +3 -4
- data/lib/active_model/attribute_methods.rb +74 -38
- data/lib/active_model/attribute_mutation_tracker.rb +8 -5
- data/lib/active_model/attribute_set.rb +18 -16
- data/lib/active_model/attribute_set/builder.rb +80 -13
- data/lib/active_model/attributes.rb +20 -24
- data/lib/active_model/callbacks.rb +1 -1
- data/lib/active_model/dirty.rb +12 -4
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +316 -208
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/naming.rb +2 -2
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +1 -1
- data/lib/active_model/secure_password.rb +14 -14
- data/lib/active_model/serialization.rb +9 -6
- data/lib/active_model/serializers/json.rb +7 -0
- data/lib/active_model/type/date_time.rb +2 -2
- data/lib/active_model/type/float.rb +2 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
- data/lib/active_model/type/helpers/numeric.rb +8 -3
- data/lib/active_model/type/helpers/time_value.rb +27 -17
- data/lib/active_model/type/helpers/timezone.rb +1 -1
- data/lib/active_model/type/immutable_string.rb +14 -10
- data/lib/active_model/type/integer.rb +11 -2
- data/lib/active_model/type/registry.rb +11 -4
- data/lib/active_model/type/string.rb +12 -2
- data/lib/active_model/type/value.rb +9 -1
- data/lib/active_model/validations.rb +6 -6
- data/lib/active_model/validations/absence.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +1 -1
- data/lib/active_model/validations/callbacks.rb +15 -15
- data/lib/active_model/validations/clusivity.rb +5 -1
- data/lib/active_model/validations/confirmation.rb +2 -2
- data/lib/active_model/validations/exclusion.rb +1 -1
- data/lib/active_model/validations/format.rb +2 -2
- data/lib/active_model/validations/inclusion.rb +1 -1
- data/lib/active_model/validations/length.rb +2 -2
- data/lib/active_model/validations/numericality.rb +48 -41
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/validates.rb +6 -4
- data/lib/active_model/validator.rb +7 -1
- metadata +13 -11
data/lib/active_model/lint.rb
CHANGED
@@ -101,7 +101,7 @@ module ActiveModel
|
|
101
101
|
# locale. If no error is present, the method should return an empty array.
|
102
102
|
def test_errors_aref
|
103
103
|
assert_respond_to model, :errors
|
104
|
-
|
104
|
+
assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
|
105
105
|
end
|
106
106
|
|
107
107
|
private
|
data/lib/active_model/naming.rb
CHANGED
@@ -8,7 +8,7 @@ module ActiveModel
|
|
8
8
|
class Name
|
9
9
|
include Comparable
|
10
10
|
|
11
|
-
|
11
|
+
attr_accessor :singular, :plural, :element, :collection,
|
12
12
|
:singular_route_key, :route_key, :param_key, :i18n_key,
|
13
13
|
:name
|
14
14
|
|
@@ -166,7 +166,7 @@ module ActiveModel
|
|
166
166
|
|
167
167
|
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
|
168
168
|
|
169
|
-
@unnamespaced = @name.
|
169
|
+
@unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
|
170
170
|
@klass = klass
|
171
171
|
@singular = _singularize(@name)
|
172
172
|
@plural = ActiveSupport::Inflector.pluralize(@singular)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/error"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module ActiveModel
|
7
|
+
class NestedError < Error
|
8
|
+
def initialize(base, inner_error, override_options = {})
|
9
|
+
@base = base
|
10
|
+
@inner_error = inner_error
|
11
|
+
@attribute = override_options.fetch(:attribute) { inner_error.attribute }
|
12
|
+
@type = override_options.fetch(:type) { inner_error.type }
|
13
|
+
@raw_type = inner_error.raw_type
|
14
|
+
@options = inner_error.options
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :inner_error
|
18
|
+
|
19
|
+
extend Forwardable
|
20
|
+
def_delegators :@inner_error, :message
|
21
|
+
end
|
22
|
+
end
|
data/lib/active_model/railtie.rb
CHANGED
@@ -14,7 +14,7 @@ module ActiveModel
|
|
14
14
|
end
|
15
15
|
|
16
16
|
initializer "active_model.i18n_customize_full_message" do
|
17
|
-
ActiveModel::
|
17
|
+
ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
@@ -45,19 +45,19 @@ module ActiveModel
|
|
45
45
|
# end
|
46
46
|
#
|
47
47
|
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
48
|
-
# user.save
|
48
|
+
# user.save # => false, password required
|
49
49
|
# user.password = 'mUc3m00RsqyRe'
|
50
|
-
# user.save
|
50
|
+
# user.save # => false, confirmation doesn't match
|
51
51
|
# user.password_confirmation = 'mUc3m00RsqyRe'
|
52
|
-
# user.save
|
52
|
+
# user.save # => true
|
53
53
|
# user.recovery_password = "42password"
|
54
|
-
# user.recovery_password_digest
|
55
|
-
# user.save
|
56
|
-
# user.authenticate('notright')
|
57
|
-
# user.authenticate('mUc3m00RsqyRe')
|
58
|
-
# user.authenticate_recovery_password('42password')
|
59
|
-
# User.find_by(name: 'david')
|
60
|
-
# User.find_by(name: 'david')
|
54
|
+
# user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
|
55
|
+
# user.save # => true
|
56
|
+
# user.authenticate('notright') # => false
|
57
|
+
# user.authenticate('mUc3m00RsqyRe') # => user
|
58
|
+
# user.authenticate_recovery_password('42password') # => user
|
59
|
+
# User.find_by(name: 'david')&.authenticate('notright') # => false
|
60
|
+
# User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
|
61
61
|
def has_secure_password(attribute = :password, validations: true)
|
62
62
|
# Load bcrypt gem only when has_secure_password is used.
|
63
63
|
# This is to avoid ActiveModel (and by extension the entire framework)
|
@@ -79,7 +79,7 @@ module ActiveModel
|
|
79
79
|
# when there is an error, the message is added to the password attribute instead
|
80
80
|
# so that the error message will make sense to the end-user.
|
81
81
|
validate do |record|
|
82
|
-
record.errors.add(attribute, :blank) unless record.
|
82
|
+
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
83
83
|
end
|
84
84
|
|
85
85
|
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
@@ -94,11 +94,11 @@ module ActiveModel
|
|
94
94
|
|
95
95
|
define_method("#{attribute}=") do |unencrypted_password|
|
96
96
|
if unencrypted_password.nil?
|
97
|
-
self.
|
97
|
+
self.public_send("#{attribute}_digest=", nil)
|
98
98
|
elsif !unencrypted_password.empty?
|
99
99
|
instance_variable_set("@#{attribute}", unencrypted_password)
|
100
100
|
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
101
|
-
self.
|
101
|
+
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
@@ -117,7 +117,7 @@ module ActiveModel
|
|
117
117
|
# user.authenticate_password('notright') # => false
|
118
118
|
# user.authenticate_password('mUc3m00RsqyRe') # => user
|
119
119
|
define_method("authenticate_#{attribute}") do |unencrypted_password|
|
120
|
-
attribute_digest =
|
120
|
+
attribute_digest = public_send("#{attribute}_digest")
|
121
121
|
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
122
122
|
end
|
123
123
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/core_ext/
|
4
|
-
require "active_support/core_ext/hash/slice"
|
3
|
+
require "active_support/core_ext/enumerable"
|
5
4
|
|
6
5
|
module ActiveModel
|
7
6
|
# == Active \Model \Serialization
|
@@ -124,17 +123,17 @@ module ActiveModel
|
|
124
123
|
# user.serializable_hash(include: { notes: { only: 'title' }})
|
125
124
|
# # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
|
126
125
|
def serializable_hash(options = nil)
|
127
|
-
options ||= {}
|
128
|
-
|
129
126
|
attribute_names = attributes.keys
|
127
|
+
|
128
|
+
return serializable_attributes(attribute_names) if options.blank?
|
129
|
+
|
130
130
|
if only = options[:only]
|
131
131
|
attribute_names &= Array(only).map(&:to_s)
|
132
132
|
elsif except = options[:except]
|
133
133
|
attribute_names -= Array(except).map(&:to_s)
|
134
134
|
end
|
135
135
|
|
136
|
-
hash =
|
137
|
-
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
|
136
|
+
hash = serializable_attributes(attribute_names)
|
138
137
|
|
139
138
|
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
|
140
139
|
|
@@ -168,6 +167,10 @@ module ActiveModel
|
|
168
167
|
# end
|
169
168
|
alias :read_attribute_for_serialization :send
|
170
169
|
|
170
|
+
def serializable_attributes(attribute_names)
|
171
|
+
attribute_names.index_with { |n| read_attribute_for_serialization(n) }
|
172
|
+
end
|
173
|
+
|
171
174
|
# Add associations specified via the <tt>:include</tt> option.
|
172
175
|
#
|
173
176
|
# Expects a block that takes as arguments:
|
@@ -42,6 +42,13 @@ module ActiveModel
|
|
42
42
|
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
43
43
|
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
|
44
44
|
#
|
45
|
+
# If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
|
46
|
+
#
|
47
|
+
# user = User.find(1)
|
48
|
+
# user.as_json(root: "author")
|
49
|
+
# # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
50
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
|
51
|
+
#
|
45
52
|
# Without any +options+, the returned Hash will include all the model's
|
46
53
|
# attributes.
|
47
54
|
#
|
@@ -35,8 +35,8 @@ module ActiveModel
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def value_from_multiparameter_assignment(values_hash)
|
38
|
-
missing_parameters =
|
39
|
-
|
38
|
+
missing_parameters = [1, 2, 3].delete_if { |key| values_hash.key?(key) }
|
39
|
+
unless missing_parameters.empty?
|
40
40
|
raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
|
41
41
|
end
|
42
42
|
super
|
@@ -4,12 +4,12 @@ module ActiveModel
|
|
4
4
|
module Type
|
5
5
|
module Helpers # :nodoc: all
|
6
6
|
class AcceptsMultiparameterTime < Module
|
7
|
-
|
8
|
-
|
7
|
+
module InstanceMethods
|
8
|
+
def serialize(value)
|
9
9
|
super(cast(value))
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
def cast(value)
|
13
13
|
if value.is_a?(Hash)
|
14
14
|
value_from_multiparameter_assignment(value)
|
15
15
|
else
|
@@ -17,7 +17,7 @@ module ActiveModel
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
|
20
|
+
def assert_valid_value(value)
|
21
21
|
if value.is_a?(Hash)
|
22
22
|
value_from_multiparameter_assignment(value)
|
23
23
|
else
|
@@ -25,17 +25,21 @@ module ActiveModel
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
|
28
|
+
def value_constructed_by_mass_assignment?(value)
|
29
29
|
value.is_a?(Hash)
|
30
30
|
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(defaults: {})
|
34
|
+
include InstanceMethods
|
31
35
|
|
32
36
|
define_method(:value_from_multiparameter_assignment) do |values_hash|
|
33
37
|
defaults.each do |k, v|
|
34
38
|
values_hash[k] ||= v
|
35
39
|
end
|
36
40
|
return unless values_hash[1] && values_hash[2] && values_hash[3]
|
37
|
-
values = values_hash.sort.map(&:last)
|
38
|
-
::Time.
|
41
|
+
values = values_hash.sort.map!(&:last)
|
42
|
+
::Time.public_send(default_timezone, *values)
|
39
43
|
end
|
40
44
|
private :value_from_multiparameter_assignment
|
41
45
|
end
|
@@ -9,13 +9,18 @@ module ActiveModel
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def cast(value)
|
12
|
-
value
|
12
|
+
# Checks whether the value is numeric. Spaceship operator
|
13
|
+
# will return nil if value is not numeric.
|
14
|
+
value = if value <=> 0
|
15
|
+
value
|
16
|
+
else
|
13
17
|
case value
|
14
18
|
when true then 1
|
15
19
|
when false then 0
|
16
|
-
|
17
|
-
else value
|
20
|
+
else value.presence
|
18
21
|
end
|
22
|
+
end
|
23
|
+
|
19
24
|
super(value)
|
20
25
|
end
|
21
26
|
|
@@ -11,10 +11,10 @@ module ActiveModel
|
|
11
11
|
value = apply_seconds_precision(value)
|
12
12
|
|
13
13
|
if value.acts_like?(:time)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
value = value.
|
14
|
+
if is_utc?
|
15
|
+
value = value.getutc if value.respond_to?(:getutc) && !value.utc?
|
16
|
+
else
|
17
|
+
value = value.getlocal if value.respond_to?(:getlocal)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -52,27 +52,37 @@ module ActiveModel
|
|
52
52
|
time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
|
53
53
|
return unless time
|
54
54
|
|
55
|
-
time -= offset
|
55
|
+
time -= offset unless offset == 0
|
56
56
|
is_utc? ? time : time.getlocal
|
57
|
+
elsif is_utc?
|
58
|
+
::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
|
57
59
|
else
|
58
|
-
::Time.
|
60
|
+
::Time.local(year, mon, mday, hour, min, sec, microsec) rescue nil
|
59
61
|
end
|
60
62
|
end
|
61
63
|
|
62
|
-
ISO_DATETIME =
|
64
|
+
ISO_DATETIME = /
|
65
|
+
\A
|
66
|
+
(\d{4})-(\d\d)-(\d\d)(?:T|\s) # 2020-06-20T
|
67
|
+
(\d\d):(\d\d):(\d\d)(?:\.(\d{1,6})\d*)? # 10:20:30.123456
|
68
|
+
(?:(Z(?=\z)|[+-]\d\d)(?::?(\d\d))?)? # +09:00
|
69
|
+
\z
|
70
|
+
/x
|
63
71
|
|
64
|
-
# Doesn't handle time zones.
|
65
72
|
def fast_string_to_time(string)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
microsec = (microsec_part.to_r * 1_000_000).to_i
|
73
|
-
end
|
74
|
-
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
|
73
|
+
return unless ISO_DATETIME =~ string
|
74
|
+
|
75
|
+
usec = $7.to_i
|
76
|
+
usec_len = $7&.length
|
77
|
+
if usec_len&.< 6
|
78
|
+
usec *= 10**(6 - usec_len)
|
75
79
|
end
|
80
|
+
|
81
|
+
if $8
|
82
|
+
offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
|
83
|
+
end
|
84
|
+
|
85
|
+
new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
|
76
86
|
end
|
77
87
|
end
|
78
88
|
end
|
@@ -3,28 +3,32 @@
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
5
|
class ImmutableString < Value # :nodoc:
|
6
|
+
def initialize(**args)
|
7
|
+
@true = -(args.delete(:true)&.to_s || "t")
|
8
|
+
@false = -(args.delete(:false)&.to_s || "f")
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
6
12
|
def type
|
7
13
|
:string
|
8
14
|
end
|
9
15
|
|
10
16
|
def serialize(value)
|
11
17
|
case value
|
12
|
-
when ::Numeric, ActiveSupport::Duration then value.to_s
|
13
|
-
when true then
|
14
|
-
when false then
|
18
|
+
when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
|
19
|
+
when true then @true
|
20
|
+
when false then @false
|
15
21
|
else super
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
19
25
|
private
|
20
26
|
def cast_value(value)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
result.freeze
|
27
|
+
case value
|
28
|
+
when true then @true
|
29
|
+
when false then @false
|
30
|
+
else value.to_s.freeze
|
31
|
+
end
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
@@ -9,7 +9,7 @@ module ActiveModel
|
|
9
9
|
# 4 bytes means an integer as opposed to smallint etc.
|
10
10
|
DEFAULT_LIMIT = 4
|
11
11
|
|
12
|
-
def initialize(
|
12
|
+
def initialize(**)
|
13
13
|
super
|
14
14
|
@range = min_value...max_value
|
15
15
|
end
|
@@ -28,15 +28,24 @@ module ActiveModel
|
|
28
28
|
ensure_in_range(super)
|
29
29
|
end
|
30
30
|
|
31
|
+
def serializable?(value)
|
32
|
+
cast_value = cast(value)
|
33
|
+
in_range?(cast_value) && super
|
34
|
+
end
|
35
|
+
|
31
36
|
private
|
32
37
|
attr_reader :range
|
33
38
|
|
39
|
+
def in_range?(value)
|
40
|
+
!value || range.member?(value)
|
41
|
+
end
|
42
|
+
|
34
43
|
def cast_value(value)
|
35
44
|
value.to_i rescue nil
|
36
45
|
end
|
37
46
|
|
38
47
|
def ensure_in_range(value)
|
39
|
-
|
48
|
+
unless in_range?(value)
|
40
49
|
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
|
41
50
|
end
|
42
51
|
value
|
@@ -8,9 +8,16 @@ module ActiveModel
|
|
8
8
|
@registrations = []
|
9
9
|
end
|
10
10
|
|
11
|
+
def initialize_dup(other)
|
12
|
+
@registrations = @registrations.dup
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
11
16
|
def register(type_name, klass = nil, **options, &block)
|
12
|
-
|
13
|
-
|
17
|
+
unless block_given?
|
18
|
+
block = proc { |_, *args| klass.new(*args) }
|
19
|
+
block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
|
20
|
+
end
|
14
21
|
registrations << registration_klass.new(type_name, block, **options)
|
15
22
|
end
|
16
23
|
|
@@ -31,8 +38,8 @@ module ActiveModel
|
|
31
38
|
Registration
|
32
39
|
end
|
33
40
|
|
34
|
-
def find_registration(symbol, *args)
|
35
|
-
registrations.find { |r| r.matches?(symbol, *args) }
|
41
|
+
def find_registration(symbol, *args, **kwargs)
|
42
|
+
registrations.find { |r| r.matches?(symbol, *args, **kwargs) }
|
36
43
|
end
|
37
44
|
end
|
38
45
|
|