activemodel 4.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. 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
- protected
20
+ private
19
21
  def sanitize_for_mass_assignment(attributes)
20
- if attributes.respond_to?(:permitted?) && !attributes.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 = 4
9
- MINOR = 2
10
+ MAJOR = 6
11
+ MINOR = 1
10
12
  TINY = 0
11
13
  PRE = nil
12
14
 
@@ -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
- # == Responds to <tt>to_key</tt>
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
- # Returns an Enumerable of all (primary) key attributes
27
- # or nil if <tt>model.persisted?</tt> is false. This is used by
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
- assert model.respond_to?(:to_key), "The model should respond to to_key"
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
- # == Responds to <tt>to_param</tt>
36
- #
37
- # Returns a string representing the object's key suitable for use in URLs
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
- assert model.respond_to?(:to_param), "The model should respond to to_param"
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
- # == Responds to <tt>to_partial_path</tt>
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
- # Returns a string giving a relative path. This is used for looking up
56
- # partials. For example, a BlogPost model might return "blog_posts/blog_post"
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
- assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
59
+ assert_respond_to model, :to_partial_path
59
60
  assert_kind_of String, model.to_partial_path
60
61
  end
61
62
 
62
- # == Responds to <tt>persisted?</tt>
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
- # Returns a boolean that specifies whether the object has been persisted
65
- # yet. This is used when calculating the URL for an object. If the object
66
- # is not persisted, a form for that object, for instance, will route to
67
- # the create action. If it is persisted, a form for the object will routes
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
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
71
+ assert_respond_to model, :persisted?
71
72
  assert_boolean model.persisted?, "persisted?"
72
73
  end
73
74
 
74
- # == \Naming
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
- # Model.model_name and Model#model_name must return a string with some
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
- assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
82
+ assert_respond_to model.class, :model_name
81
83
  model_name = model.class.model_name
82
- assert model_name.respond_to?(:to_str)
83
- assert model_name.human.respond_to?(:to_str)
84
- assert model_name.singular.respond_to?(:to_str)
85
- assert model_name.plural.respond_to?(:to_str)
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
- assert model.respond_to?(:model_name), "The model instance should respond to model_name"
89
+ assert_respond_to model, :model_name
88
90
  assert_equal model.model_name, model.class.model_name
89
91
  end
90
92
 
91
- # == \Errors Testing
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
- # Returns an object that implements [](attribute) defined which returns an
94
- # Array of Strings that are the errors for the attribute in question.
95
- # If localization is used, the Strings should be localized for the current
96
- # locale. If no error is present, this method should return an empty Array.
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
- assert model.respond_to?(:errors), "The model should respond to errors"
99
- assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
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
- assert @model.respond_to?(:to_model), "The object should respond to to_model"
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)"
@@ -1,12 +1,13 @@
1
- module ActiveModel
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
- # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
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 <tt>ActiveRecord</tt> does.
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(params={})
79
- params.each do |attr, value|
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
@@ -1,11 +1,14 @@
1
- require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/module/introspection'
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
- attr_reader :singular, :plural, :element, :collection,
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
- # ==(other)
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.sub(/^#{namespace.name}::/, '') if namespace
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 humane format, using I18n. By default,
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
- def _singularize(string, replacement='_')
193
- ActiveSupport::Inflector.underscore(string).tr('/', replacement)
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.remove_possible_method :model_name
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
- # include ActiveModel::Model
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 = self.parents.detect do |n|
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
@@ -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 characters, and if we pass
6
- # password of length more than 72 characters it ignores extra characters.
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 +password_digest+ attribute.
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 characters
22
- # * Confirmation of password (using a +password_confirmation+ attribute)
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 password confirmation validation is not needed, simply leave out the
25
- # value for +password_confirmation+ (i.e. don't provide a form field 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 supress the default
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 # => false, password required
48
+ # user.save # => false, password required
45
49
  # user.password = 'mUc3m00RsqyRe'
46
- # user.save # => false, confirmation doesn't match
50
+ # user.save # => false, confirmation doesn't match
47
51
  # user.password_confirmation = 'mUc3m00RsqyRe'
48
- # user.save # => true
49
- # user.authenticate('notright') # => false
50
- # user.authenticate('mUc3m00RsqyRe') # => user
51
- # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
52
- # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
53
- def has_secure_password(options = {})
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 'bcrypt'
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 options.fetch(:validations, true)
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(:password, :blank) unless record.password_digest.present?
82
+ record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
75
83
  end
76
84
 
77
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
78
- validates_confirmation_of :password, allow_blank: true
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
- module InstanceMethodsOnActivation
91
- # Returns +self+ if the password is correct, otherwise +false+.
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
- attr_reader :password
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
- # Encrypts the password into the +password_digest+ attribute, only if the
108
- # new password is not empty.
109
- #
110
- # class User < ActiveRecord::Base
111
- # has_secure_password validations: false
112
- # end
113
- #
114
- # user = User.new
115
- # user.password = nil
116
- # user.password_digest # => nil
117
- # user.password = 'mUc3m00RsqyRe'
118
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
119
- def password=(unencrypted_password)
120
- if unencrypted_password.nil?
121
- self.password_digest = nil
122
- elsif !unencrypted_password.empty?
123
- @password = unencrypted_password
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
- def password_confirmation=(unencrypted_password)
130
- @password_confirmation = unencrypted_password
124
+ alias_method :authenticate, :authenticate_password if attribute == :password
131
125
  end
132
126
  end
133
127
  end