activemodel 5.2.6 → 6.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -109
- data/MIT-LICENSE +1 -1
- data/README.rdoc +6 -4
- data/lib/active_model.rb +2 -1
- data/lib/active_model/attribute.rb +21 -21
- data/lib/active_model/attribute/user_provided_default.rb +1 -2
- data/lib/active_model/attribute_assignment.rb +4 -6
- data/lib/active_model/attribute_methods.rb +117 -40
- data/lib/active_model/attribute_mutation_tracker.rb +90 -33
- data/lib/active_model/attribute_set.rb +20 -28
- data/lib/active_model/attribute_set/builder.rb +81 -16
- data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
- data/lib/active_model/attributes.rb +65 -44
- data/lib/active_model/callbacks.rb +11 -9
- data/lib/active_model/conversion.rb +1 -1
- data/lib/active_model/dirty.rb +51 -101
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +347 -155
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/naming.rb +22 -7
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +6 -0
- data/lib/active_model/secure_password.rb +54 -55
- data/lib/active_model/serialization.rb +9 -7
- data/lib/active_model/serializers/json.rb +17 -9
- data/lib/active_model/translation.rb +1 -1
- data/lib/active_model/type/big_integer.rb +0 -1
- data/lib/active_model/type/binary.rb +1 -1
- data/lib/active_model/type/boolean.rb +0 -1
- data/lib/active_model/type/date.rb +0 -5
- data/lib/active_model/type/date_time.rb +3 -8
- data/lib/active_model/type/decimal.rb +0 -1
- data/lib/active_model/type/float.rb +2 -3
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
- data/lib/active_model/type/helpers/numeric.rb +17 -6
- data/lib/active_model/type/helpers/time_value.rb +37 -15
- data/lib/active_model/type/helpers/timezone.rb +1 -1
- data/lib/active_model/type/immutable_string.rb +14 -11
- data/lib/active_model/type/integer.rb +15 -18
- data/lib/active_model/type/registry.rb +16 -16
- data/lib/active_model/type/string.rb +12 -3
- data/lib/active_model/type/time.rb +1 -6
- data/lib/active_model/type/value.rb +9 -2
- data/lib/active_model/validations.rb +6 -9
- data/lib/active_model/validations/absence.rb +2 -2
- data/lib/active_model/validations/acceptance.rb +34 -27
- data/lib/active_model/validations/callbacks.rb +15 -16
- data/lib/active_model/validations/clusivity.rb +6 -3
- data/lib/active_model/validations/confirmation.rb +4 -4
- data/lib/active_model/validations/exclusion.rb +1 -1
- data/lib/active_model/validations/format.rb +2 -3
- data/lib/active_model/validations/inclusion.rb +2 -2
- data/lib/active_model/validations/length.rb +3 -3
- data/lib/active_model/validations/numericality.rb +58 -44
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/validates.rb +7 -6
- data/lib/active_model/validator.rb +8 -3
- metadata +14 -9
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
|
|
@@ -110,6 +110,22 @@ module ActiveModel
|
|
110
110
|
# BlogPost.model_name.eql?('BlogPost') # => true
|
111
111
|
# BlogPost.model_name.eql?('Blog Post') # => false
|
112
112
|
|
113
|
+
##
|
114
|
+
# :method: match?
|
115
|
+
#
|
116
|
+
# :call-seq:
|
117
|
+
# match?(regexp)
|
118
|
+
#
|
119
|
+
# Equivalent to <tt>String#match?</tt>. Match the class name against the
|
120
|
+
# given regexp. Returns +true+ if there is a match, otherwise +false+.
|
121
|
+
#
|
122
|
+
# class BlogPost
|
123
|
+
# extend ActiveModel::Naming
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# BlogPost.model_name.match?(/Post/) # => true
|
127
|
+
# BlogPost.model_name.match?(/\d/) # => false
|
128
|
+
|
113
129
|
##
|
114
130
|
# :method: to_s
|
115
131
|
#
|
@@ -131,7 +147,7 @@ module ActiveModel
|
|
131
147
|
# to_str()
|
132
148
|
#
|
133
149
|
# Equivalent to +to_s+.
|
134
|
-
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
|
150
|
+
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
|
135
151
|
:to_str, :as_json, to: :name
|
136
152
|
|
137
153
|
# Returns a new ActiveModel::Name instance. By default, the +namespace+
|
@@ -150,7 +166,7 @@ module ActiveModel
|
|
150
166
|
|
151
167
|
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
|
152
168
|
|
153
|
-
@unnamespaced = @name.
|
169
|
+
@unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
|
154
170
|
@klass = klass
|
155
171
|
@singular = _singularize(@name)
|
156
172
|
@plural = ActiveSupport::Inflector.pluralize(@singular)
|
@@ -187,13 +203,12 @@ module ActiveModel
|
|
187
203
|
defaults << @human
|
188
204
|
|
189
205
|
options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
|
190
|
-
I18n.translate(defaults.shift, options)
|
206
|
+
I18n.translate(defaults.shift, **options)
|
191
207
|
end
|
192
208
|
|
193
209
|
private
|
194
|
-
|
195
210
|
def _singularize(string)
|
196
|
-
ActiveSupport::Inflector.underscore(string).tr("/"
|
211
|
+
ActiveSupport::Inflector.underscore(string).tr("/", "_")
|
197
212
|
end
|
198
213
|
end
|
199
214
|
|
@@ -236,7 +251,7 @@ module ActiveModel
|
|
236
251
|
# Person.model_name.plural # => "people"
|
237
252
|
def model_name
|
238
253
|
@_model_name ||= begin
|
239
|
-
namespace =
|
254
|
+
namespace = module_parents.detect do |n|
|
240
255
|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
241
256
|
end
|
242
257
|
ActiveModel::Name.new(self, namespace)
|
@@ -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
@@ -7,8 +7,14 @@ module ActiveModel
|
|
7
7
|
class Railtie < Rails::Railtie # :nodoc:
|
8
8
|
config.eager_load_namespaces << ActiveModel
|
9
9
|
|
10
|
+
config.active_model = ActiveSupport::OrderedOptions.new
|
11
|
+
|
10
12
|
initializer "active_model.secure_password" do
|
11
13
|
ActiveModel::SecurePassword.min_cost = Rails.env.test?
|
12
14
|
end
|
15
|
+
|
16
|
+
initializer "active_model.i18n_customize_full_message" do
|
17
|
+
ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
|
18
|
+
end
|
13
19
|
end
|
14
20
|
end
|
@@ -16,15 +16,16 @@ module ActiveModel
|
|
16
16
|
|
17
17
|
module ClassMethods
|
18
18
|
# Adds methods to set and authenticate against a BCrypt password.
|
19
|
-
# This mechanism requires you to have a +
|
19
|
+
# This mechanism requires you to have a +XXX_digest+ attribute.
|
20
|
+
# Where +XXX+ is the attribute name of your desired password.
|
20
21
|
#
|
21
22
|
# The following validations are added automatically:
|
22
23
|
# * Password must be present on creation
|
23
24
|
# * Password length should be less than or equal to 72 bytes
|
24
|
-
# * Confirmation of password (using a +
|
25
|
+
# * Confirmation of password (using a +XXX_confirmation+ attribute)
|
25
26
|
#
|
26
|
-
# If
|
27
|
-
# value for +
|
27
|
+
# If confirmation validation is not needed, simply leave out the
|
28
|
+
# value for +XXX_confirmation+ (i.e. don't provide a form field for
|
28
29
|
# it). When this attribute has a +nil+ value, the validation will not be
|
29
30
|
# triggered.
|
30
31
|
#
|
@@ -37,22 +38,27 @@ module ActiveModel
|
|
37
38
|
#
|
38
39
|
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
39
40
|
#
|
40
|
-
# # Schema: User(name:string, password_digest:string)
|
41
|
+
# # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
|
41
42
|
# class User < ActiveRecord::Base
|
42
43
|
# has_secure_password
|
44
|
+
# has_secure_password :recovery_password, validations: false
|
43
45
|
# end
|
44
46
|
#
|
45
47
|
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
46
|
-
# user.save
|
48
|
+
# user.save # => false, password required
|
47
49
|
# user.password = 'mUc3m00RsqyRe'
|
48
|
-
# user.save
|
50
|
+
# user.save # => false, confirmation doesn't match
|
49
51
|
# user.password_confirmation = 'mUc3m00RsqyRe'
|
50
|
-
# user.save
|
51
|
-
# user.
|
52
|
-
# user.
|
53
|
-
#
|
54
|
-
#
|
55
|
-
|
52
|
+
# user.save # => true
|
53
|
+
# user.recovery_password = "42password"
|
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
|
+
def has_secure_password(attribute = :password, validations: true)
|
56
62
|
# Load bcrypt gem only when has_secure_password is used.
|
57
63
|
# This is to avoid ActiveModel (and by extension the entire framework)
|
58
64
|
# being dependent on a binary library.
|
@@ -63,9 +69,9 @@ module ActiveModel
|
|
63
69
|
raise
|
64
70
|
end
|
65
71
|
|
66
|
-
include InstanceMethodsOnActivation
|
72
|
+
include InstanceMethodsOnActivation.new(attribute)
|
67
73
|
|
68
|
-
if
|
74
|
+
if validations
|
69
75
|
include ActiveModel::Validations
|
70
76
|
|
71
77
|
# This ensures the model has a password by checking whether the password_digest
|
@@ -73,56 +79,49 @@ module ActiveModel
|
|
73
79
|
# when there is an error, the message is added to the password attribute instead
|
74
80
|
# so that the error message will make sense to the end-user.
|
75
81
|
validate do |record|
|
76
|
-
record.errors.add(
|
82
|
+
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
77
83
|
end
|
78
84
|
|
79
|
-
validates_length_of
|
80
|
-
validates_confirmation_of
|
85
|
+
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
86
|
+
validates_confirmation_of attribute, allow_blank: true
|
81
87
|
end
|
82
88
|
end
|
83
89
|
end
|
84
90
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
# class User < ActiveRecord::Base
|
89
|
-
# has_secure_password validations: false
|
90
|
-
# end
|
91
|
-
#
|
92
|
-
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
93
|
-
# user.save
|
94
|
-
# user.authenticate('notright') # => false
|
95
|
-
# user.authenticate('mUc3m00RsqyRe') # => user
|
96
|
-
def authenticate(unencrypted_password)
|
97
|
-
BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
|
98
|
-
end
|
91
|
+
class InstanceMethodsOnActivation < Module
|
92
|
+
def initialize(attribute)
|
93
|
+
attr_reader attribute
|
99
94
|
|
100
|
-
|
95
|
+
define_method("#{attribute}=") do |unencrypted_password|
|
96
|
+
if unencrypted_password.nil?
|
97
|
+
self.public_send("#{attribute}_digest=", nil)
|
98
|
+
elsif !unencrypted_password.empty?
|
99
|
+
instance_variable_set("@#{attribute}", unencrypted_password)
|
100
|
+
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
101
|
+
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
|
102
|
+
end
|
103
|
+
end
|
101
104
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
120
|
-
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
|
105
|
+
define_method("#{attribute}_confirmation=") do |unencrypted_password|
|
106
|
+
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns +self+ if the password is correct, otherwise +false+.
|
110
|
+
#
|
111
|
+
# class User < ActiveRecord::Base
|
112
|
+
# has_secure_password validations: false
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
116
|
+
# user.save
|
117
|
+
# user.authenticate_password('notright') # => false
|
118
|
+
# user.authenticate_password('mUc3m00RsqyRe') # => user
|
119
|
+
define_method("authenticate_#{attribute}") do |unencrypted_password|
|
120
|
+
attribute_digest = public_send("#{attribute}_digest")
|
121
|
+
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
121
122
|
end
|
122
|
-
end
|
123
123
|
|
124
|
-
|
125
|
-
@password_confirmation = unencrypted_password
|
124
|
+
alias_method :authenticate, :authenticate_password if attribute == :password
|
126
125
|
end
|
127
126
|
end
|
128
127
|
end
|
@@ -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
|
|
@@ -150,7 +149,6 @@ module ActiveModel
|
|
150
149
|
end
|
151
150
|
|
152
151
|
private
|
153
|
-
|
154
152
|
# Hook method defining how an attribute value should be retrieved for
|
155
153
|
# serialization. By default this is assumed to be an instance named after
|
156
154
|
# the attribute. Override this method in subclasses should you need to
|
@@ -169,6 +167,10 @@ module ActiveModel
|
|
169
167
|
# end
|
170
168
|
alias :read_attribute_for_serialization :send
|
171
169
|
|
170
|
+
def serializable_attributes(attribute_names)
|
171
|
+
attribute_names.index_with { |n| read_attribute_for_serialization(n) }
|
172
|
+
end
|
173
|
+
|
172
174
|
# Add associations specified via the <tt>:include</tt> option.
|
173
175
|
#
|
174
176
|
# Expects a block that takes as arguments:
|
@@ -26,13 +26,13 @@ module ActiveModel
|
|
26
26
|
# user = User.find(1)
|
27
27
|
# user.as_json
|
28
28
|
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
29
|
-
# # "created_at" => "2006
|
29
|
+
# # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
|
30
30
|
#
|
31
31
|
# ActiveRecord::Base.include_root_in_json = true
|
32
32
|
#
|
33
33
|
# user.as_json
|
34
34
|
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
35
|
-
# # "created_at" => "2006
|
35
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
|
36
36
|
#
|
37
37
|
# This behavior can also be achieved by setting the <tt>:root</tt> option
|
38
38
|
# to +true+ as in:
|
@@ -40,7 +40,14 @@ module ActiveModel
|
|
40
40
|
# user = User.find(1)
|
41
41
|
# user.as_json(root: true)
|
42
42
|
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
43
|
-
# # "created_at" => "2006
|
43
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
|
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 } }
|
44
51
|
#
|
45
52
|
# Without any +options+, the returned Hash will include all the model's
|
46
53
|
# attributes.
|
@@ -48,7 +55,7 @@ module ActiveModel
|
|
48
55
|
# user = User.find(1)
|
49
56
|
# user.as_json
|
50
57
|
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
51
|
-
# # "created_at" => "2006
|
58
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
|
52
59
|
#
|
53
60
|
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
|
54
61
|
# the attributes included, and work similar to the +attributes+ method.
|
@@ -63,14 +70,14 @@ module ActiveModel
|
|
63
70
|
#
|
64
71
|
# user.as_json(methods: :permalink)
|
65
72
|
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
66
|
-
# # "created_at" => "2006
|
73
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
|
67
74
|
# # "permalink" => "1-konata-izumi" }
|
68
75
|
#
|
69
76
|
# To include associations use <tt>:include</tt>:
|
70
77
|
#
|
71
78
|
# user.as_json(include: :posts)
|
72
79
|
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
73
|
-
# # "created_at" => "2006
|
80
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
|
74
81
|
# # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
|
75
82
|
# # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
|
76
83
|
#
|
@@ -81,7 +88,7 @@ module ActiveModel
|
|
81
88
|
# only: :body } },
|
82
89
|
# only: :title } })
|
83
90
|
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
|
84
|
-
# # "created_at" => "2006
|
91
|
+
# # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
|
85
92
|
# # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
|
86
93
|
# # "title" => "Welcome to the weblog" },
|
87
94
|
# # { "comments" => [ { "body" => "Don't think too hard" } ],
|
@@ -93,11 +100,12 @@ module ActiveModel
|
|
93
100
|
include_root_in_json
|
94
101
|
end
|
95
102
|
|
103
|
+
hash = serializable_hash(options).as_json
|
96
104
|
if root
|
97
105
|
root = model_name.element if root == true
|
98
|
-
{ root =>
|
106
|
+
{ root => hash }
|
99
107
|
else
|
100
|
-
|
108
|
+
hash
|
101
109
|
end
|
102
110
|
end
|
103
111
|
|