activemodel 5.2.6 → 6.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -109
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -4
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +4 -6
  9. data/lib/active_model/attribute_methods.rb +117 -40
  10. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  11. data/lib/active_model/attribute_set.rb +20 -28
  12. data/lib/active_model/attribute_set/builder.rb +81 -16
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +65 -44
  15. data/lib/active_model/callbacks.rb +11 -9
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +51 -101
  18. data/lib/active_model/error.rb +207 -0
  19. data/lib/active_model/errors.rb +347 -155
  20. data/lib/active_model/gem_version.rb +3 -3
  21. data/lib/active_model/lint.rb +1 -1
  22. data/lib/active_model/naming.rb +22 -7
  23. data/lib/active_model/nested_error.rb +22 -0
  24. data/lib/active_model/railtie.rb +6 -0
  25. data/lib/active_model/secure_password.rb +54 -55
  26. data/lib/active_model/serialization.rb +9 -7
  27. data/lib/active_model/serializers/json.rb +17 -9
  28. data/lib/active_model/translation.rb +1 -1
  29. data/lib/active_model/type/big_integer.rb +0 -1
  30. data/lib/active_model/type/binary.rb +1 -1
  31. data/lib/active_model/type/boolean.rb +0 -1
  32. data/lib/active_model/type/date.rb +0 -5
  33. data/lib/active_model/type/date_time.rb +3 -8
  34. data/lib/active_model/type/decimal.rb +0 -1
  35. data/lib/active_model/type/float.rb +2 -3
  36. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  37. data/lib/active_model/type/helpers/numeric.rb +17 -6
  38. data/lib/active_model/type/helpers/time_value.rb +37 -15
  39. data/lib/active_model/type/helpers/timezone.rb +1 -1
  40. data/lib/active_model/type/immutable_string.rb +14 -11
  41. data/lib/active_model/type/integer.rb +15 -18
  42. data/lib/active_model/type/registry.rb +16 -16
  43. data/lib/active_model/type/string.rb +12 -3
  44. data/lib/active_model/type/time.rb +1 -6
  45. data/lib/active_model/type/value.rb +9 -2
  46. data/lib/active_model/validations.rb +6 -9
  47. data/lib/active_model/validations/absence.rb +2 -2
  48. data/lib/active_model/validations/acceptance.rb +34 -27
  49. data/lib/active_model/validations/callbacks.rb +15 -16
  50. data/lib/active_model/validations/clusivity.rb +6 -3
  51. data/lib/active_model/validations/confirmation.rb +4 -4
  52. data/lib/active_model/validations/exclusion.rb +1 -1
  53. data/lib/active_model/validations/format.rb +2 -3
  54. data/lib/active_model/validations/inclusion.rb +2 -2
  55. data/lib/active_model/validations/length.rb +3 -3
  56. data/lib/active_model/validations/numericality.rb +58 -44
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/validates.rb +7 -6
  59. data/lib/active_model/validator.rb +8 -3
  60. metadata +14 -9
@@ -34,7 +34,6 @@ module ActiveModel
34
34
  end
35
35
 
36
36
  private
37
-
38
37
  def cast_value(value)
39
38
  if value == ""
40
39
  nil
@@ -10,16 +10,11 @@ module ActiveModel
10
10
  :date
11
11
  end
12
12
 
13
- def serialize(value)
14
- cast(value)
15
- end
16
-
17
13
  def type_cast_for_schema(value)
18
14
  value.to_s(:db).inspect
19
15
  end
20
16
 
21
17
  private
22
-
23
18
  def cast_value(value)
24
19
  if value.is_a?(::String)
25
20
  return if value.empty?
@@ -13,12 +13,7 @@ module ActiveModel
13
13
  :datetime
14
14
  end
15
15
 
16
- def serialize(value)
17
- super(cast(value))
18
- end
19
-
20
16
  private
21
-
22
17
  def cast_value(value)
23
18
  return apply_seconds_precision(value) unless value.is_a?(::String)
24
19
  return if value.empty?
@@ -40,9 +35,9 @@ module ActiveModel
40
35
  end
41
36
 
42
37
  def value_from_multiparameter_assignment(values_hash)
43
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
44
- if missing_parameter
45
- raise ArgumentError, missing_parameter
38
+ missing_parameters = [1, 2, 3].delete_if { |key| values_hash.key?(key) }
39
+ unless missing_parameters.empty?
40
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
46
41
  end
47
42
  super
48
43
  end
@@ -17,7 +17,6 @@ module ActiveModel
17
17
  end
18
18
 
19
19
  private
20
-
21
20
  def cast_value(value)
22
21
  casted_value = \
23
22
  case value
@@ -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:
@@ -18,10 +20,7 @@ module ActiveModel
18
20
  end
19
21
  end
20
22
 
21
- alias serialize cast
22
-
23
23
  private
24
-
25
24
  def cast_value(value)
26
25
  case value
27
26
  when ::Float then value
@@ -4,8 +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(:cast) do |value|
7
+ module InstanceMethods
8
+ def serialize(value)
9
+ super(cast(value))
10
+ end
11
+
12
+ def cast(value)
9
13
  if value.is_a?(Hash)
10
14
  value_from_multiparameter_assignment(value)
11
15
  else
@@ -13,7 +17,7 @@ module ActiveModel
13
17
  end
14
18
  end
15
19
 
16
- define_method(:assert_valid_value) do |value|
20
+ def assert_valid_value(value)
17
21
  if value.is_a?(Hash)
18
22
  value_from_multiparameter_assignment(value)
19
23
  else
@@ -21,17 +25,21 @@ module ActiveModel
21
25
  end
22
26
  end
23
27
 
24
- define_method(:value_constructed_by_mass_assignment?) do |value|
28
+ def value_constructed_by_mass_assignment?(value)
25
29
  value.is_a?(Hash)
26
30
  end
31
+ end
32
+
33
+ def initialize(defaults: {})
34
+ include InstanceMethods
27
35
 
28
36
  define_method(:value_from_multiparameter_assignment) do |values_hash|
29
37
  defaults.each do |k, v|
30
38
  values_hash[k] ||= v
31
39
  end
32
40
  return unless values_hash[1] && values_hash[2] && values_hash[3]
33
- values = values_hash.sort.map(&:last)
34
- ::Time.send(default_timezone, *values)
41
+ values = values_hash.sort.map!(&:last)
42
+ ::Time.public_send(default_timezone, *values)
35
43
  end
36
44
  private :value_from_multiparameter_assignment
37
45
  end
@@ -4,14 +4,23 @@ module ActiveModel
4
4
  module Type
5
5
  module Helpers # :nodoc: all
6
6
  module Numeric
7
+ def serialize(value)
8
+ cast(value)
9
+ end
10
+
7
11
  def cast(value)
8
- 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
9
17
  case value
10
18
  when true then 1
11
19
  when false then 0
12
- when ::String then value.presence
13
- else value
20
+ else value.presence
14
21
  end
22
+ end
23
+
15
24
  super(value)
16
25
  end
17
26
 
@@ -20,17 +29,19 @@ module ActiveModel
20
29
  end
21
30
 
22
31
  private
23
-
24
32
  def number_to_non_number?(old_value, new_value_before_type_cast)
25
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
33
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
26
34
  end
27
35
 
28
36
  def non_numeric_string?(value)
29
37
  # 'wibble'.to_i will give zero, we want to make sure
30
38
  # that we aren't marking int zero to string zero as
31
39
  # changed.
32
- !/\A[-+]?\d+/.match?(value.to_s)
40
+ !NUMERIC_REGEX.match?(value)
33
41
  end
42
+
43
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
44
+ private_constant :NUMERIC_REGEX
34
45
  end
35
46
  end
36
47
  end
@@ -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
 
@@ -22,10 +22,17 @@ module ActiveModel
22
22
  end
23
23
 
24
24
  def apply_seconds_precision(value)
25
- return value unless precision && value.respond_to?(:usec)
26
- number_of_insignificant_digits = 6 - precision
25
+ return value unless precision && value.respond_to?(:nsec)
26
+
27
+ number_of_insignificant_digits = 9 - precision
27
28
  round_power = 10**number_of_insignificant_digits
28
- value.change(usec: value.usec - value.usec % round_power)
29
+ rounded_off_nsec = value.nsec % round_power
30
+
31
+ if rounded_off_nsec > 0
32
+ value.change(nsec: value.nsec - rounded_off_nsec)
33
+ else
34
+ value
35
+ end
29
36
  end
30
37
 
31
38
  def type_cast_for_schema(value)
@@ -37,7 +44,6 @@ module ActiveModel
37
44
  end
38
45
 
39
46
  private
40
-
41
47
  def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
42
48
  # Treat 0000-00-00 00:00:00 as nil.
43
49
  return if year.nil? || (year == 0 && mon == 0 && mday == 0)
@@ -46,21 +52,37 @@ module ActiveModel
46
52
  time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
47
53
  return unless time
48
54
 
49
- time -= offset
55
+ time -= offset unless offset == 0
50
56
  is_utc? ? time : time.getlocal
57
+ elsif is_utc?
58
+ ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
51
59
  else
52
- ::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
53
61
  end
54
62
  end
55
63
 
56
- 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
57
71
 
58
- # Doesn't handle time zones.
59
72
  def fast_string_to_time(string)
60
- if string =~ ISO_DATETIME
61
- microsec = ($7.to_r * 1_000_000).to_i
62
- 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)
63
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)
64
86
  end
65
87
  end
66
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,29 +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
-
21
26
  def cast_value(value)
22
- result = \
23
- case value
24
- when true then "t"
25
- when false then "f"
26
- else value.to_s
27
- end
28
- result.freeze
27
+ case value
28
+ when true then @true
29
+ when false then @false
30
+ else value.to_s.freeze
31
+ end
29
32
  end
30
33
  end
31
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
@@ -19,39 +19,36 @@ module ActiveModel
19
19
  end
20
20
 
21
21
  def deserialize(value)
22
- return if value.nil?
22
+ return if value.blank?
23
23
  value.to_i
24
24
  end
25
25
 
26
26
  def serialize(value)
27
- result = cast(value)
28
- if result
29
- ensure_in_range(result)
30
- end
31
- result
27
+ return if value.is_a?(::String) && non_numeric_string?(value)
28
+ ensure_in_range(super)
32
29
  end
33
30
 
34
- # TODO Change this to private once we've dropped Ruby 2.2 support.
35
- # Workaround for Ruby 2.2 "private attribute?" warning.
36
- protected
31
+ def serializable?(value)
32
+ cast_value = cast(value)
33
+ in_range?(cast_value) && super
34
+ end
37
35
 
36
+ private
38
37
  attr_reader :range
39
38
 
40
- private
39
+ def in_range?(value)
40
+ !value || range.member?(value)
41
+ end
41
42
 
42
43
  def cast_value(value)
43
- case value
44
- when true then 1
45
- when false then 0
46
- else
47
- value.to_i rescue nil
48
- end
44
+ value.to_i rescue nil
49
45
  end
50
46
 
51
47
  def ensure_in_range(value)
52
- unless range.cover?(value)
48
+ unless in_range?(value)
53
49
  raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
54
50
  end
51
+ value
55
52
  end
56
53
 
57
54
  def max_value
@@ -8,35 +8,38 @@ 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) }
17
+ unless block_given?
18
+ block = proc { |_, *args| klass.new(*args) }
19
+ block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
20
+ end
13
21
  registrations << registration_klass.new(type_name, block, **options)
14
22
  end
15
23
 
16
- def lookup(symbol, *args)
17
- registration = find_registration(symbol, *args)
24
+ def lookup(symbol, *args, **kwargs)
25
+ registration = find_registration(symbol, *args, **kwargs)
18
26
 
19
27
  if registration
20
- registration.call(self, symbol, *args)
28
+ registration.call(self, symbol, *args, **kwargs)
21
29
  else
22
30
  raise ArgumentError, "Unknown type #{symbol.inspect}"
23
31
  end
24
32
  end
25
33
 
26
- # TODO Change this to private once we've dropped Ruby 2.2 support.
27
- # Workaround for Ruby 2.2 "private attribute?" warning.
28
- protected
29
-
30
- attr_reader :registrations
31
-
32
34
  private
35
+ attr_reader :registrations
33
36
 
34
37
  def registration_klass
35
38
  Registration
36
39
  end
37
40
 
38
- def find_registration(symbol, *args)
39
- registrations.find { |r| r.matches?(symbol, *args) }
41
+ def find_registration(symbol, *args, **kwargs)
42
+ registrations.find { |r| r.matches?(symbol, *args, **kwargs) }
40
43
  end
41
44
  end
42
45
 
@@ -59,10 +62,7 @@ module ActiveModel
59
62
  type_name == name
60
63
  end
61
64
 
62
- # TODO Change this to private once we've dropped Ruby 2.2 support.
63
- # Workaround for Ruby 2.2 "private attribute?" warning.
64
- protected
65
-
65
+ private
66
66
  attr_reader :name, :block
67
67
  end
68
68
  end