activemodel 6.0.3.2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -182
  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/dirty.rb +12 -4
  14. data/lib/active_model/error.rb +207 -0
  15. data/lib/active_model/errors.rb +316 -208
  16. data/lib/active_model/gem_version.rb +3 -3
  17. data/lib/active_model/lint.rb +1 -1
  18. data/lib/active_model/naming.rb +2 -2
  19. data/lib/active_model/nested_error.rb +22 -0
  20. data/lib/active_model/railtie.rb +1 -1
  21. data/lib/active_model/secure_password.rb +14 -14
  22. data/lib/active_model/serialization.rb +9 -6
  23. data/lib/active_model/serializers/json.rb +7 -0
  24. data/lib/active_model/type/date_time.rb +2 -2
  25. data/lib/active_model/type/float.rb +2 -0
  26. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  27. data/lib/active_model/type/helpers/numeric.rb +8 -3
  28. data/lib/active_model/type/helpers/time_value.rb +27 -17
  29. data/lib/active_model/type/helpers/timezone.rb +1 -1
  30. data/lib/active_model/type/immutable_string.rb +14 -10
  31. data/lib/active_model/type/integer.rb +11 -2
  32. data/lib/active_model/type/registry.rb +11 -4
  33. data/lib/active_model/type/string.rb +12 -2
  34. data/lib/active_model/type/value.rb +9 -1
  35. data/lib/active_model/validations.rb +6 -6
  36. data/lib/active_model/validations/absence.rb +1 -1
  37. data/lib/active_model/validations/acceptance.rb +1 -1
  38. data/lib/active_model/validations/clusivity.rb +5 -1
  39. data/lib/active_model/validations/confirmation.rb +2 -2
  40. data/lib/active_model/validations/exclusion.rb +1 -1
  41. data/lib/active_model/validations/format.rb +2 -2
  42. data/lib/active_model/validations/inclusion.rb +1 -1
  43. data/lib/active_model/validations/length.rb +2 -2
  44. data/lib/active_model/validations/numericality.rb +48 -41
  45. data/lib/active_model/validations/presence.rb +1 -1
  46. data/lib/active_model/validations/validates.rb +6 -4
  47. data/lib/active_model/validator.rb +7 -1
  48. 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 = "2"
11
+ MINOR = 1
12
+ TINY = 0
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