activemodel 4.2.0 → 6.1.0
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 +5 -5
- data/CHANGELOG.md +49 -37
- data/MIT-LICENSE +1 -1
- data/README.rdoc +16 -22
- data/lib/active_model/attribute/user_provided_default.rb +51 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute_assignment.rb +55 -0
- data/lib/active_model/attribute_methods.rb +150 -73
- data/lib/active_model/attribute_mutation_tracker.rb +181 -0
- data/lib/active_model/attribute_set/builder.rb +191 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +106 -0
- data/lib/active_model/attributes.rb +132 -0
- data/lib/active_model/callbacks.rb +31 -25
- data/lib/active_model/conversion.rb +20 -9
- data/lib/active_model/dirty.rb +142 -116
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +436 -202
- data/lib/active_model/forbidden_attributes_protection.rb +6 -3
- data/lib/active_model/gem_version.rb +5 -3
- data/lib/active_model/lint.rb +47 -42
- data/lib/active_model/locale/en.yml +2 -1
- data/lib/active_model/model.rb +7 -7
- data/lib/active_model/naming.rb +36 -18
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +8 -0
- data/lib/active_model/secure_password.rb +61 -67
- data/lib/active_model/serialization.rb +48 -17
- data/lib/active_model/serializers/json.rb +22 -13
- data/lib/active_model/translation.rb +5 -4
- data/lib/active_model/type/big_integer.rb +14 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +46 -0
- data/lib/active_model/type/date.rb +52 -0
- data/lib/active_model/type/date_time.rb +46 -0
- data/lib/active_model/type/decimal.rb +69 -0
- data/lib/active_model/type/float.rb +35 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +48 -0
- data/lib/active_model/type/helpers/time_value.rb +90 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +35 -0
- data/lib/active_model/type/integer.rb +67 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +35 -0
- data/lib/active_model/type/time.rb +46 -0
- data/lib/active_model/type/value.rb +133 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/validations/absence.rb +6 -4
- data/lib/active_model/validations/acceptance.rb +72 -14
- data/lib/active_model/validations/callbacks.rb +23 -19
- data/lib/active_model/validations/clusivity.rb +18 -12
- data/lib/active_model/validations/confirmation.rb +27 -14
- data/lib/active_model/validations/exclusion.rb +7 -4
- data/lib/active_model/validations/format.rb +27 -27
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +8 -7
- data/lib/active_model/validations/length.rb +35 -32
- data/lib/active_model/validations/numericality.rb +72 -34
- data/lib/active_model/validations/presence.rb +3 -3
- data/lib/active_model/validations/validates.rb +17 -15
- data/lib/active_model/validations/with.rb +6 -12
- data/lib/active_model/validations.rb +58 -23
- data/lib/active_model/validator.rb +23 -17
- data/lib/active_model/version.rb +4 -2
- data/lib/active_model.rb +18 -11
- metadata +44 -25
- data/lib/active_model/serializers/xml.rb +0 -238
- data/lib/active_model/test_case.rb +0 -4
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
# Raised when forbidden attributes are used for mass assignment.
|
3
5
|
#
|
@@ -15,10 +17,11 @@ module ActiveModel
|
|
15
17
|
end
|
16
18
|
|
17
19
|
module ForbiddenAttributesProtection # :nodoc:
|
18
|
-
|
20
|
+
private
|
19
21
|
def sanitize_for_mass_assignment(attributes)
|
20
|
-
if attributes.respond_to?(:permitted?)
|
21
|
-
raise ActiveModel::ForbiddenAttributesError
|
22
|
+
if attributes.respond_to?(:permitted?)
|
23
|
+
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
|
24
|
+
attributes.to_h
|
22
25
|
else
|
23
26
|
attributes
|
24
27
|
end
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
|
-
# Returns the version of the currently loaded Active Model as a <tt>Gem::Version</tt>
|
4
|
+
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
|
3
5
|
def self.gem_version
|
4
6
|
Gem::Version.new VERSION::STRING
|
5
7
|
end
|
6
8
|
|
7
9
|
module VERSION
|
8
|
-
MAJOR =
|
9
|
-
MINOR =
|
10
|
+
MAJOR = 6
|
11
|
+
MINOR = 1
|
10
12
|
TINY = 0
|
11
13
|
PRE = nil
|
12
14
|
|
data/lib/active_model/lint.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module Lint
|
3
5
|
# == Active \Model \Lint \Tests
|
@@ -20,88 +22,91 @@ module ActiveModel
|
|
20
22
|
# to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
|
21
23
|
# +self+.
|
22
24
|
module Tests
|
23
|
-
|
24
|
-
#
|
25
|
+
# Passes if the object's model responds to <tt>to_key</tt> and if calling
|
26
|
+
# this method returns +nil+ when the object is not persisted.
|
27
|
+
# Fails otherwise.
|
25
28
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# <tt>dom_id</tt> to generate unique ids for the object.
|
29
|
+
# <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
|
30
|
+
# of the model, and is used to a generate unique DOM id for the object.
|
29
31
|
def test_to_key
|
30
|
-
|
32
|
+
assert_respond_to model, :to_key
|
31
33
|
def model.persisted?() false end
|
32
34
|
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
|
33
35
|
end
|
34
36
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# or +nil+ if <tt>model.persisted?</tt> is +false+.
|
37
|
+
# Passes if the object's model responds to <tt>to_param</tt> and if
|
38
|
+
# calling this method returns +nil+ when the object is not persisted.
|
39
|
+
# Fails otherwise.
|
39
40
|
#
|
41
|
+
# <tt>to_param</tt> is used to represent the object's key in URLs.
|
40
42
|
# Implementers can decide to either raise an exception or provide a
|
41
43
|
# default in case the record uses a composite primary key. There are no
|
42
44
|
# tests for this behavior in lint because it doesn't make sense to force
|
43
45
|
# any of the possible implementation strategies on the implementer.
|
44
|
-
# However, if the resource is not persisted?, then <tt>to_param</tt>
|
45
|
-
# should always return +nil+.
|
46
46
|
def test_to_param
|
47
|
-
|
47
|
+
assert_respond_to model, :to_param
|
48
48
|
def model.to_key() [1] end
|
49
49
|
def model.persisted?() false end
|
50
50
|
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
|
51
51
|
end
|
52
52
|
|
53
|
-
#
|
53
|
+
# Passes if the object's model responds to <tt>to_partial_path</tt> and if
|
54
|
+
# calling this method returns a string. Fails otherwise.
|
54
55
|
#
|
55
|
-
#
|
56
|
-
#
|
56
|
+
# <tt>to_partial_path</tt> is used for looking up partials. For example,
|
57
|
+
# a BlogPost model might return "blog_posts/blog_post".
|
57
58
|
def test_to_partial_path
|
58
|
-
|
59
|
+
assert_respond_to model, :to_partial_path
|
59
60
|
assert_kind_of String, model.to_partial_path
|
60
61
|
end
|
61
62
|
|
62
|
-
#
|
63
|
+
# Passes if the object's model responds to <tt>persisted?</tt> and if
|
64
|
+
# calling this method returns either +true+ or +false+. Fails otherwise.
|
63
65
|
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# to the update action.
|
66
|
+
# <tt>persisted?</tt> is used when calculating the URL for an object.
|
67
|
+
# If the object is not persisted, a form for that object, for instance,
|
68
|
+
# will route to the create action. If it is persisted, a form for the
|
69
|
+
# object will route to the update action.
|
69
70
|
def test_persisted?
|
70
|
-
|
71
|
+
assert_respond_to model, :persisted?
|
71
72
|
assert_boolean model.persisted?, "persisted?"
|
72
73
|
end
|
73
74
|
|
74
|
-
#
|
75
|
+
# Passes if the object's model responds to <tt>model_name</tt> both as
|
76
|
+
# an instance method and as a class method, and if calling this method
|
77
|
+
# returns a string with some convenience methods: <tt>:human</tt>,
|
78
|
+
# <tt>:singular</tt> and <tt>:plural</tt>.
|
75
79
|
#
|
76
|
-
#
|
77
|
-
# convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and
|
78
|
-
# <tt>:plural</tt>. Check ActiveModel::Naming for more information.
|
80
|
+
# Check ActiveModel::Naming for more information.
|
79
81
|
def test_model_naming
|
80
|
-
|
82
|
+
assert_respond_to model.class, :model_name
|
81
83
|
model_name = model.class.model_name
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
assert_respond_to model_name, :to_str
|
85
|
+
assert_respond_to model_name.human, :to_str
|
86
|
+
assert_respond_to model_name.singular, :to_str
|
87
|
+
assert_respond_to model_name.plural, :to_str
|
86
88
|
|
87
|
-
|
89
|
+
assert_respond_to model, :model_name
|
88
90
|
assert_equal model.model_name, model.class.model_name
|
89
91
|
end
|
90
92
|
|
91
|
-
#
|
93
|
+
# Passes if the object's model responds to <tt>errors</tt> and if calling
|
94
|
+
# <tt>[](attribute)</tt> on the result of this method returns an array.
|
95
|
+
# Fails otherwise.
|
92
96
|
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
+
# <tt>errors[attribute]</tt> is used to retrieve the errors of a model
|
98
|
+
# for a given attribute. If errors are present, the method should return
|
99
|
+
# an array of strings that are the errors for the attribute in question.
|
100
|
+
# If localization is used, the strings should be localized for the current
|
101
|
+
# locale. If no error is present, the method should return an empty array.
|
97
102
|
def test_errors_aref
|
98
|
-
|
99
|
-
|
103
|
+
assert_respond_to model, :errors
|
104
|
+
assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
|
100
105
|
end
|
101
106
|
|
102
107
|
private
|
103
108
|
def model
|
104
|
-
|
109
|
+
assert_respond_to @model, :to_model
|
105
110
|
@model.to_model
|
106
111
|
end
|
107
112
|
|
@@ -6,6 +6,7 @@ en:
|
|
6
6
|
# The values :model, :attribute and :value are always available for interpolation
|
7
7
|
# The value :count is available when applicable. Can be used for pluralization.
|
8
8
|
messages:
|
9
|
+
model_invalid: "Validation failed: %{errors}"
|
9
10
|
inclusion: "is not included in the list"
|
10
11
|
exclusion: "is reserved"
|
11
12
|
invalid: "is invalid"
|
@@ -16,7 +17,7 @@ en:
|
|
16
17
|
present: "must be blank"
|
17
18
|
too_long:
|
18
19
|
one: "is too long (maximum is 1 character)"
|
19
|
-
other: "is too long (maximum is %{count} characters)"
|
20
|
+
other: "is too long (maximum is %{count} characters)"
|
20
21
|
too_short:
|
21
22
|
one: "is too short (minimum is 1 character)"
|
22
23
|
other: "is too short (minimum is %{count} characters)"
|
data/lib/active_model/model.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module ActiveModel
|
3
4
|
# == Active \Model \Basic \Model
|
4
5
|
#
|
5
6
|
# Includes the required interface for an object to interact with
|
6
|
-
#
|
7
|
+
# Action Pack and Action View, using different Active Model modules.
|
7
8
|
# It includes model name introspections, conversions, translations and
|
8
9
|
# validations. Besides that, it allows you to initialize the object with a
|
9
|
-
# hash of attributes, pretty much like
|
10
|
+
# hash of attributes, pretty much like Active Record does.
|
10
11
|
#
|
11
12
|
# A minimal implementation could be:
|
12
13
|
#
|
@@ -57,6 +58,7 @@ module ActiveModel
|
|
57
58
|
# (see below).
|
58
59
|
module Model
|
59
60
|
extend ActiveSupport::Concern
|
61
|
+
include ActiveModel::AttributeAssignment
|
60
62
|
include ActiveModel::Validations
|
61
63
|
include ActiveModel::Conversion
|
62
64
|
|
@@ -75,10 +77,8 @@ module ActiveModel
|
|
75
77
|
# person = Person.new(name: 'bob', age: '18')
|
76
78
|
# person.name # => "bob"
|
77
79
|
# person.age # => "18"
|
78
|
-
def initialize(
|
79
|
-
|
80
|
-
self.public_send("#{attr}=", value)
|
81
|
-
end if params
|
80
|
+
def initialize(attributes = {})
|
81
|
+
assign_attributes(attributes) if attributes
|
82
82
|
|
83
83
|
super()
|
84
84
|
end
|
data/lib/active_model/naming.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "active_support/core_ext/module/introspection"
|
5
|
+
require "active_support/core_ext/module/redefine_method"
|
3
6
|
|
4
7
|
module ActiveModel
|
5
8
|
class Name
|
6
9
|
include Comparable
|
7
10
|
|
8
|
-
|
11
|
+
attr_accessor :singular, :plural, :element, :collection,
|
9
12
|
:singular_route_key, :route_key, :param_key, :i18n_key,
|
10
13
|
:name
|
11
14
|
|
@@ -46,7 +49,7 @@ module ActiveModel
|
|
46
49
|
# :method: <=>
|
47
50
|
#
|
48
51
|
# :call-seq:
|
49
|
-
#
|
52
|
+
# <=>(other)
|
50
53
|
#
|
51
54
|
# Equivalent to <tt>String#<=></tt>.
|
52
55
|
#
|
@@ -107,6 +110,22 @@ module ActiveModel
|
|
107
110
|
# BlogPost.model_name.eql?('BlogPost') # => true
|
108
111
|
# BlogPost.model_name.eql?('Blog Post') # => false
|
109
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
|
+
|
110
129
|
##
|
111
130
|
# :method: to_s
|
112
131
|
#
|
@@ -128,8 +147,8 @@ module ActiveModel
|
|
128
147
|
# to_str()
|
129
148
|
#
|
130
149
|
# Equivalent to +to_s+.
|
131
|
-
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
|
132
|
-
:to_str, to: :name
|
150
|
+
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
|
151
|
+
:to_str, :as_json, to: :name
|
133
152
|
|
134
153
|
# Returns a new ActiveModel::Name instance. By default, the +namespace+
|
135
154
|
# and +name+ option will take the namespace and name of the given class
|
@@ -147,7 +166,7 @@ module ActiveModel
|
|
147
166
|
|
148
167
|
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
|
149
168
|
|
150
|
-
@unnamespaced = @name.
|
169
|
+
@unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
|
151
170
|
@klass = klass
|
152
171
|
@singular = _singularize(@name)
|
153
172
|
@plural = ActiveSupport::Inflector.pluralize(@singular)
|
@@ -162,7 +181,7 @@ module ActiveModel
|
|
162
181
|
@route_key << "_index" if @plural == @singular
|
163
182
|
end
|
164
183
|
|
165
|
-
# Transform the model name into a more
|
184
|
+
# Transform the model name into a more human format, using I18n. By default,
|
166
185
|
# it will underscore then humanize the class name.
|
167
186
|
#
|
168
187
|
# class BlogPost
|
@@ -172,7 +191,7 @@ module ActiveModel
|
|
172
191
|
# BlogPost.model_name.human # => "Blog post"
|
173
192
|
#
|
174
193
|
# Specify +options+ with additional translating options.
|
175
|
-
def human(options={})
|
194
|
+
def human(options = {})
|
176
195
|
return @human unless @klass.respond_to?(:lookup_ancestors) &&
|
177
196
|
@klass.respond_to?(:i18n_scope)
|
178
197
|
|
@@ -184,14 +203,13 @@ module ActiveModel
|
|
184
203
|
defaults << @human
|
185
204
|
|
186
205
|
options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
|
187
|
-
I18n.translate(defaults.shift, options)
|
206
|
+
I18n.translate(defaults.shift, **options)
|
188
207
|
end
|
189
208
|
|
190
209
|
private
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
end
|
210
|
+
def _singularize(string)
|
211
|
+
ActiveSupport::Inflector.underscore(string).tr("/", "_")
|
212
|
+
end
|
195
213
|
end
|
196
214
|
|
197
215
|
# == Active \Model \Naming
|
@@ -211,11 +229,11 @@ module ActiveModel
|
|
211
229
|
# BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
|
212
230
|
#
|
213
231
|
# Providing the functionality that ActiveModel::Naming provides in your object
|
214
|
-
# is required to pass the Active Model Lint test. So either extending the
|
232
|
+
# is required to pass the \Active \Model Lint test. So either extending the
|
215
233
|
# provided method below, or rolling your own is required.
|
216
234
|
module Naming
|
217
235
|
def self.extended(base) #:nodoc:
|
218
|
-
base.
|
236
|
+
base.silence_redefinition_of_method :model_name
|
219
237
|
base.delegate :model_name, to: :class
|
220
238
|
end
|
221
239
|
|
@@ -224,7 +242,7 @@ module ActiveModel
|
|
224
242
|
# (See ActiveModel::Name for more information).
|
225
243
|
#
|
226
244
|
# class Person
|
227
|
-
#
|
245
|
+
# extend ActiveModel::Naming
|
228
246
|
# end
|
229
247
|
#
|
230
248
|
# Person.model_name.name # => "Person"
|
@@ -233,7 +251,7 @@ module ActiveModel
|
|
233
251
|
# Person.model_name.plural # => "people"
|
234
252
|
def model_name
|
235
253
|
@_model_name ||= begin
|
236
|
-
namespace =
|
254
|
+
namespace = module_parents.detect do |n|
|
237
255
|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
238
256
|
end
|
239
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
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_model"
|
2
4
|
require "rails"
|
3
5
|
|
@@ -5,8 +7,14 @@ module ActiveModel
|
|
5
7
|
class Railtie < Rails::Railtie # :nodoc:
|
6
8
|
config.eager_load_namespaces << ActiveModel
|
7
9
|
|
10
|
+
config.active_model = ActiveSupport::OrderedOptions.new
|
11
|
+
|
8
12
|
initializer "active_model.secure_password" do
|
9
13
|
ActiveModel::SecurePassword.min_cost = Rails.env.test?
|
10
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
|
11
19
|
end
|
12
20
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module SecurePassword
|
3
5
|
extend ActiveSupport::Concern
|
4
6
|
|
5
|
-
# BCrypt hash function can handle maximum 72
|
6
|
-
# password of length more than 72
|
7
|
+
# BCrypt hash function can handle maximum 72 bytes, and if we pass
|
8
|
+
# password of length more than 72 bytes it ignores extra characters.
|
7
9
|
# Hence need to put a restriction on password length.
|
8
10
|
MAX_PASSWORD_LENGTH_ALLOWED = 72
|
9
11
|
|
@@ -14,19 +16,20 @@ module ActiveModel
|
|
14
16
|
|
15
17
|
module ClassMethods
|
16
18
|
# Adds methods to set and authenticate against a BCrypt password.
|
17
|
-
# 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.
|
18
21
|
#
|
19
22
|
# The following validations are added automatically:
|
20
23
|
# * Password must be present on creation
|
21
|
-
# * Password length should be less than or equal to 72
|
22
|
-
# * Confirmation of password (using a +
|
24
|
+
# * Password length should be less than or equal to 72 bytes
|
25
|
+
# * Confirmation of password (using a +XXX_confirmation+ attribute)
|
23
26
|
#
|
24
|
-
# If
|
25
|
-
# 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
|
26
29
|
# it). When this attribute has a +nil+ value, the validation will not be
|
27
30
|
# triggered.
|
28
31
|
#
|
29
|
-
# For further customizability, it is possible to
|
32
|
+
# For further customizability, it is possible to suppress the default
|
30
33
|
# validations by passing <tt>validations: false</tt> as an argument.
|
31
34
|
#
|
32
35
|
# Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
|
@@ -35,35 +38,40 @@ module ActiveModel
|
|
35
38
|
#
|
36
39
|
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
37
40
|
#
|
38
|
-
# # Schema: User(name:string, password_digest:string)
|
41
|
+
# # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
|
39
42
|
# class User < ActiveRecord::Base
|
40
43
|
# has_secure_password
|
44
|
+
# has_secure_password :recovery_password, validations: false
|
41
45
|
# end
|
42
46
|
#
|
43
47
|
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
44
|
-
# user.save
|
48
|
+
# user.save # => false, password required
|
45
49
|
# user.password = 'mUc3m00RsqyRe'
|
46
|
-
# user.save
|
50
|
+
# user.save # => false, confirmation doesn't match
|
47
51
|
# user.password_confirmation = 'mUc3m00RsqyRe'
|
48
|
-
# user.save
|
49
|
-
# user.
|
50
|
-
# user.
|
51
|
-
#
|
52
|
-
#
|
53
|
-
|
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)
|
54
62
|
# Load bcrypt gem only when has_secure_password is used.
|
55
63
|
# This is to avoid ActiveModel (and by extension the entire framework)
|
56
64
|
# being dependent on a binary library.
|
57
65
|
begin
|
58
|
-
require
|
66
|
+
require "bcrypt"
|
59
67
|
rescue LoadError
|
60
68
|
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
|
61
69
|
raise
|
62
70
|
end
|
63
71
|
|
64
|
-
include InstanceMethodsOnActivation
|
72
|
+
include InstanceMethodsOnActivation.new(attribute)
|
65
73
|
|
66
|
-
if
|
74
|
+
if validations
|
67
75
|
include ActiveModel::Validations
|
68
76
|
|
69
77
|
# This ensures the model has a password by checking whether the password_digest
|
@@ -71,63 +79,49 @@ module ActiveModel
|
|
71
79
|
# when there is an error, the message is added to the password attribute instead
|
72
80
|
# so that the error message will make sense to the end-user.
|
73
81
|
validate do |record|
|
74
|
-
record.errors.add(
|
82
|
+
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
75
83
|
end
|
76
84
|
|
77
|
-
validates_length_of
|
78
|
-
validates_confirmation_of
|
79
|
-
end
|
80
|
-
|
81
|
-
# This code is necessary as long as the protected_attributes gem is supported.
|
82
|
-
if respond_to?(:attributes_protected_by_default)
|
83
|
-
def self.attributes_protected_by_default #:nodoc:
|
84
|
-
super + ['password_digest']
|
85
|
-
end
|
85
|
+
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
86
|
+
validates_confirmation_of attribute, allow_blank: true
|
86
87
|
end
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# class User < ActiveRecord::Base
|
94
|
-
# has_secure_password validations: false
|
95
|
-
# end
|
96
|
-
#
|
97
|
-
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
98
|
-
# user.save
|
99
|
-
# user.authenticate('notright') # => false
|
100
|
-
# user.authenticate('mUc3m00RsqyRe') # => user
|
101
|
-
def authenticate(unencrypted_password)
|
102
|
-
BCrypt::Password.new(password_digest) == unencrypted_password && self
|
103
|
-
end
|
91
|
+
class InstanceMethodsOnActivation < Module
|
92
|
+
def initialize(attribute)
|
93
|
+
attr_reader attribute
|
104
94
|
|
105
|
-
|
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
|
106
104
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
125
|
-
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
|
126
122
|
end
|
127
|
-
end
|
128
123
|
|
129
|
-
|
130
|
-
@password_confirmation = unencrypted_password
|
124
|
+
alias_method :authenticate, :authenticate_password if attribute == :password
|
131
125
|
end
|
132
126
|
end
|
133
127
|
end
|