activemodel 5.2.6 → 6.1.4

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 (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