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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -183
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +15 -14
  7. data/lib/active_model/attribute_assignment.rb +3 -4
  8. data/lib/active_model/attribute_methods.rb +74 -38
  9. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attribute_set/builder.rb +80 -13
  12. data/lib/active_model/attributes.rb +20 -24
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/dirty.rb +12 -4
  15. data/lib/active_model/error.rb +207 -0
  16. data/lib/active_model/errors.rb +316 -208
  17. data/lib/active_model/gem_version.rb +3 -3
  18. data/lib/active_model/lint.rb +1 -1
  19. data/lib/active_model/naming.rb +2 -2
  20. data/lib/active_model/nested_error.rb +22 -0
  21. data/lib/active_model/railtie.rb +1 -1
  22. data/lib/active_model/secure_password.rb +14 -14
  23. data/lib/active_model/serialization.rb +9 -6
  24. data/lib/active_model/serializers/json.rb +7 -0
  25. data/lib/active_model/type/date_time.rb +2 -2
  26. data/lib/active_model/type/float.rb +2 -0
  27. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  28. data/lib/active_model/type/helpers/numeric.rb +8 -3
  29. data/lib/active_model/type/helpers/time_value.rb +27 -17
  30. data/lib/active_model/type/helpers/timezone.rb +1 -1
  31. data/lib/active_model/type/immutable_string.rb +14 -10
  32. data/lib/active_model/type/integer.rb +11 -2
  33. data/lib/active_model/type/registry.rb +11 -4
  34. data/lib/active_model/type/string.rb +12 -2
  35. data/lib/active_model/type/value.rb +9 -1
  36. data/lib/active_model/validations.rb +6 -6
  37. data/lib/active_model/validations/absence.rb +1 -1
  38. data/lib/active_model/validations/acceptance.rb +1 -1
  39. data/lib/active_model/validations/callbacks.rb +15 -15
  40. data/lib/active_model/validations/clusivity.rb +5 -1
  41. data/lib/active_model/validations/confirmation.rb +2 -2
  42. data/lib/active_model/validations/exclusion.rb +1 -1
  43. data/lib/active_model/validations/format.rb +2 -2
  44. data/lib/active_model/validations/inclusion.rb +1 -1
  45. data/lib/active_model/validations/length.rb +2 -2
  46. data/lib/active_model/validations/numericality.rb +48 -41
  47. data/lib/active_model/validations/presence.rb +1 -1
  48. data/lib/active_model/validations/validates.rb +6 -4
  49. data/lib/active_model/validator.rb +7 -1
  50. metadata +13 -11
@@ -8,9 +8,9 @@ module ActiveModel
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 6
11
- MINOR = 0
12
- TINY = 3
13
- PRE = "3"
11
+ MINOR = 1
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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
- assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
104
+ assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
105
105
  end
106
106
 
107
107
  private
@@ -8,7 +8,7 @@ module ActiveModel
8
8
  class Name
9
9
  include Comparable
10
10
 
11
- attr_reader :singular, :plural, :element, :collection,
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.sub(/^#{namespace.name}::/, "") if namespace
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
@@ -14,7 +14,7 @@ module ActiveModel
14
14
  end
15
15
 
16
16
  initializer "active_model.i18n_customize_full_message" do
17
- ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
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 # => false, password required
48
+ # user.save # => false, password required
49
49
  # user.password = 'mUc3m00RsqyRe'
50
- # user.save # => false, confirmation doesn't match
50
+ # user.save # => false, confirmation doesn't match
51
51
  # user.password_confirmation = 'mUc3m00RsqyRe'
52
- # user.save # => true
52
+ # user.save # => true
53
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').try(:authenticate, 'notright') # => false
60
- # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
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.send("#{attribute}_digest").present?
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.send("#{attribute}_digest=", nil)
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.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
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 = send("#{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/hash/except"
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 = (1..3).select { |key| !values_hash.key?(key) }
39
- if missing_parameters.any?
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/try"
4
+
3
5
  module ActiveModel
4
6
  module Type
5
7
  class Float < Value # :nodoc:
@@ -4,12 +4,12 @@ module ActiveModel
4
4
  module Type
5
5
  module Helpers # :nodoc: all
6
6
  class AcceptsMultiparameterTime < Module
7
- def initialize(defaults: {})
8
- define_method(:serialize) do |value|
7
+ module InstanceMethods
8
+ def serialize(value)
9
9
  super(cast(value))
10
10
  end
11
11
 
12
- define_method(:cast) do |value|
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
- define_method(:assert_valid_value) do |value|
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
- define_method(:value_constructed_by_mass_assignment?) do |value|
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.send(default_timezone, *values)
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
- when ::String then value.presence
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
- zone_conversion_method = is_utc? ? :getutc : :getlocal
15
-
16
- if value.respond_to?(zone_conversion_method)
17
- value = value.send(zone_conversion_method)
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.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
60
+ ::Time.local(year, mon, mday, hour, min, sec, microsec) rescue nil
59
61
  end
60
62
  end
61
63
 
62
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
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
- if string =~ ISO_DATETIME
67
- microsec_part = $7
68
- if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7
69
- microsec_part[0] = ""
70
- microsec = microsec_part.to_i
71
- else
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
@@ -7,7 +7,7 @@ module ActiveModel
7
7
  module Helpers # :nodoc: all
8
8
  module Timezone
9
9
  def is_utc?
10
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
10
+ ::Time.zone_default.nil? || ::Time.zone_default.match?("UTC")
11
11
  end
12
12
 
13
13
  def default_timezone
@@ -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 "t"
14
- when false then "f"
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
- result = \
22
- case value
23
- when true then "t"
24
- when false then "f"
25
- else value.to_s
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
- if value && !range.cover?(value)
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
- block ||= proc { |_, *args| klass.new(*args) }
13
- block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
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