activemodel 5.2.2.1 → 6.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -51
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -2
  5. data/lib/active_model.rb +1 -1
  6. data/lib/active_model/attribute.rb +3 -4
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +1 -1
  9. data/lib/active_model/attribute_methods.rb +54 -15
  10. data/lib/active_model/attribute_mutation_tracker.rb +88 -34
  11. data/lib/active_model/attribute_set.rb +2 -10
  12. data/lib/active_model/attribute_set/builder.rb +1 -3
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +60 -33
  15. data/lib/active_model/callbacks.rb +10 -7
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +36 -99
  18. data/lib/active_model/errors.rb +104 -20
  19. data/lib/active_model/gem_version.rb +3 -3
  20. data/lib/active_model/naming.rb +19 -3
  21. data/lib/active_model/railtie.rb +6 -0
  22. data/lib/active_model/secure_password.rb +47 -48
  23. data/lib/active_model/serializers/json.rb +10 -9
  24. data/lib/active_model/type/binary.rb +1 -1
  25. data/lib/active_model/type/boolean.rb +10 -1
  26. data/lib/active_model/type/date.rb +2 -5
  27. data/lib/active_model/type/date_time.rb +4 -7
  28. data/lib/active_model/type/float.rb +0 -2
  29. data/lib/active_model/type/helpers.rb +1 -0
  30. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +4 -0
  31. data/lib/active_model/type/helpers/numeric.rb +9 -2
  32. data/lib/active_model/type/helpers/time_value.rb +16 -15
  33. data/lib/active_model/type/helpers/timezone.rb +19 -0
  34. data/lib/active_model/type/integer.rb +7 -19
  35. data/lib/active_model/type/registry.rb +2 -10
  36. data/lib/active_model/type/string.rb +2 -2
  37. data/lib/active_model/type/time.rb +2 -1
  38. data/lib/active_model/validations.rb +0 -2
  39. data/lib/active_model/validations/acceptance.rb +33 -25
  40. data/lib/active_model/validations/clusivity.rb +1 -1
  41. data/lib/active_model/validations/confirmation.rb +2 -2
  42. data/lib/active_model/validations/inclusion.rb +1 -1
  43. data/lib/active_model/validations/length.rb +1 -1
  44. data/lib/active_model/validations/numericality.rb +20 -11
  45. data/lib/active_model/validations/validates.rb +2 -2
  46. data/lib/active_model/validator.rb +1 -1
  47. metadata +13 -9
@@ -40,7 +40,7 @@ module ActiveModel
40
40
  alias_method :to_str, :to_s
41
41
 
42
42
  def hex
43
- @value.unpack("H*")[0]
43
+ @value.unpack1("H*")
44
44
  end
45
45
 
46
46
  def ==(other)
@@ -14,7 +14,16 @@ module ActiveModel
14
14
  # - Empty strings are coerced to +nil+
15
15
  # - All other values will be coerced to +true+
16
16
  class Boolean < Value
17
- FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
17
+ FALSE_VALUES = [
18
+ false, 0,
19
+ "0", :"0",
20
+ "f", :f,
21
+ "F", :F,
22
+ "false", :false,
23
+ "FALSE", :FALSE,
24
+ "off", :off,
25
+ "OFF", :OFF,
26
+ ].to_set.freeze
18
27
 
19
28
  def type # :nodoc:
20
29
  :boolean
@@ -3,16 +3,13 @@
3
3
  module ActiveModel
4
4
  module Type
5
5
  class Date < Value # :nodoc:
6
+ include Helpers::Timezone
6
7
  include Helpers::AcceptsMultiparameterTime.new
7
8
 
8
9
  def type
9
10
  :date
10
11
  end
11
12
 
12
- def serialize(value)
13
- cast(value)
14
- end
15
-
16
13
  def type_cast_for_schema(value)
17
14
  value.to_s(:db).inspect
18
15
  end
@@ -49,7 +46,7 @@ module ActiveModel
49
46
 
50
47
  def value_from_multiparameter_assignment(*)
51
48
  time = super
52
- time && time.to_date
49
+ time && new_date(time.year, time.mon, time.mday)
53
50
  end
54
51
  end
55
52
  end
@@ -3,6 +3,7 @@
3
3
  module ActiveModel
4
4
  module Type
5
5
  class DateTime < Value # :nodoc:
6
+ include Helpers::Timezone
6
7
  include Helpers::TimeValue
7
8
  include Helpers::AcceptsMultiparameterTime.new(
8
9
  defaults: { 4 => 0, 5 => 0 }
@@ -12,10 +13,6 @@ module ActiveModel
12
13
  :datetime
13
14
  end
14
15
 
15
- def serialize(value)
16
- super(cast(value))
17
- end
18
-
19
16
  private
20
17
 
21
18
  def cast_value(value)
@@ -39,9 +36,9 @@ module ActiveModel
39
36
  end
40
37
 
41
38
  def value_from_multiparameter_assignment(values_hash)
42
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
43
- if missing_parameter
44
- raise ArgumentError, missing_parameter
39
+ missing_parameters = (1..3).select { |key| !values_hash.key?(key) }
40
+ if missing_parameters.any?
41
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
45
42
  end
46
43
  super
47
44
  end
@@ -18,8 +18,6 @@ module ActiveModel
18
18
  end
19
19
  end
20
20
 
21
- alias serialize cast
22
-
23
21
  private
24
22
 
25
23
  def cast_value(value)
@@ -4,3 +4,4 @@ require "active_model/type/helpers/accepts_multiparameter_time"
4
4
  require "active_model/type/helpers/numeric"
5
5
  require "active_model/type/helpers/mutable"
6
6
  require "active_model/type/helpers/time_value"
7
+ require "active_model/type/helpers/timezone"
@@ -5,6 +5,10 @@ module ActiveModel
5
5
  module Helpers # :nodoc: all
6
6
  class AcceptsMultiparameterTime < Module
7
7
  def initialize(defaults: {})
8
+ define_method(:serialize) do |value|
9
+ super(cast(value))
10
+ end
11
+
8
12
  define_method(:cast) do |value|
9
13
  if value.is_a?(Hash)
10
14
  value_from_multiparameter_assignment(value)
@@ -4,6 +4,10 @@ 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
12
  value = \
9
13
  case value
@@ -22,15 +26,18 @@ module ActiveModel
22
26
  private
23
27
 
24
28
  def number_to_non_number?(old_value, new_value_before_type_cast)
25
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
29
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
26
30
  end
27
31
 
28
32
  def non_numeric_string?(value)
29
33
  # 'wibble'.to_i will give zero, we want to make sure
30
34
  # that we aren't marking int zero to string zero as
31
35
  # changed.
32
- !/\A[-+]?\d+/.match?(value.to_s)
36
+ !NUMERIC_REGEX.match?(value)
33
37
  end
38
+
39
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
40
+ private_constant :NUMERIC_REGEX
34
41
  end
35
42
  end
36
43
  end
@@ -21,25 +21,20 @@ module ActiveModel
21
21
  value
22
22
  end
23
23
 
24
- def is_utc?
25
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
26
- end
24
+ def apply_seconds_precision(value)
25
+ return value unless precision && value.respond_to?(:nsec)
26
+
27
+ number_of_insignificant_digits = 9 - precision
28
+ round_power = 10**number_of_insignificant_digits
29
+ rounded_off_nsec = value.nsec % round_power
27
30
 
28
- def default_timezone
29
- if is_utc?
30
- :utc
31
+ if rounded_off_nsec > 0
32
+ value.change(nsec: value.nsec - rounded_off_nsec)
31
33
  else
32
- :local
34
+ value
33
35
  end
34
36
  end
35
37
 
36
- def apply_seconds_precision(value)
37
- return value unless precision && value.respond_to?(:usec)
38
- number_of_insignificant_digits = 6 - precision
39
- round_power = 10**number_of_insignificant_digits
40
- value.change(usec: value.usec - value.usec % round_power)
41
- end
42
-
43
38
  def type_cast_for_schema(value)
44
39
  value.to_s(:db).inspect
45
40
  end
@@ -70,7 +65,13 @@ module ActiveModel
70
65
  # Doesn't handle time zones.
71
66
  def fast_string_to_time(string)
72
67
  if string =~ ISO_DATETIME
73
- microsec = ($7.to_r * 1_000_000).to_i
68
+ microsec_part = $7
69
+ if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7
70
+ microsec_part[0] = ""
71
+ microsec = microsec_part.to_i
72
+ else
73
+ microsec = (microsec_part.to_r * 1_000_000).to_i
74
+ end
74
75
  new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
75
76
  end
76
77
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/time/zones"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ module Helpers # :nodoc: all
8
+ module Timezone
9
+ def is_utc?
10
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
11
+ end
12
+
13
+ def default_timezone
14
+ is_utc? ? :utc : :local
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -19,39 +19,27 @@ 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
37
-
38
- attr_reader :range
39
-
40
31
  private
32
+ attr_reader :range
41
33
 
42
34
  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
35
+ value.to_i rescue nil
49
36
  end
50
37
 
51
38
  def ensure_in_range(value)
52
- unless range.cover?(value)
39
+ if value && !range.cover?(value)
53
40
  raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
54
41
  end
42
+ value
55
43
  end
56
44
 
57
45
  def max_value
@@ -23,13 +23,8 @@ module ActiveModel
23
23
  end
24
24
  end
25
25
 
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
26
  private
27
+ attr_reader :registrations
33
28
 
34
29
  def registration_klass
35
30
  Registration
@@ -59,10 +54,7 @@ module ActiveModel
59
54
  type_name == name
60
55
  end
61
56
 
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
-
57
+ private
66
58
  attr_reader :name, :block
67
59
  end
68
60
  end
@@ -16,8 +16,8 @@ module ActiveModel
16
16
  def cast_value(value)
17
17
  case value
18
18
  when ::String then ::String.new(value)
19
- when true then "t".freeze
20
- when false then "f".freeze
19
+ when true then "t"
20
+ when false then "f"
21
21
  else value.to_s
22
22
  end
23
23
  end
@@ -3,9 +3,10 @@
3
3
  module ActiveModel
4
4
  module Type
5
5
  class Time < Value # :nodoc:
6
+ include Helpers::Timezone
6
7
  include Helpers::TimeValue
7
8
  include Helpers::AcceptsMultiparameterTime.new(
8
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
9
+ defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
9
10
  )
10
11
 
11
12
  def type
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/array/extract_options"
4
- require "active_support/core_ext/hash/keys"
5
- require "active_support/core_ext/hash/except"
6
4
 
7
5
  module ActiveModel
8
6
  # == Active \Model \Validations
@@ -17,7 +17,8 @@ module ActiveModel
17
17
  private
18
18
 
19
19
  def setup!(klass)
20
- klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
20
+ define_attributes = LazilyDefineAttributes.new(attributes)
21
+ klass.include(define_attributes) unless klass.included_modules.include?(define_attributes)
21
22
  end
22
23
 
23
24
  def acceptable_option?(value)
@@ -25,50 +26,57 @@ module ActiveModel
25
26
  end
26
27
 
27
28
  class LazilyDefineAttributes < Module
28
- def initialize(attribute_definition)
29
+ def initialize(attributes)
30
+ @attributes = attributes.map(&:to_s)
31
+ end
32
+
33
+ def included(klass)
34
+ @lock = Mutex.new
35
+ mod = self
36
+
29
37
  define_method(:respond_to_missing?) do |method_name, include_private = false|
30
- super(method_name, include_private) || attribute_definition.matches?(method_name)
38
+ mod.define_on(klass)
39
+ super(method_name, include_private) || mod.matches?(method_name)
31
40
  end
32
41
 
33
42
  define_method(:method_missing) do |method_name, *args, &block|
34
- if attribute_definition.matches?(method_name)
35
- attribute_definition.define_on(self.class)
43
+ mod.define_on(klass)
44
+ if mod.matches?(method_name)
36
45
  send(method_name, *args, &block)
37
46
  else
38
47
  super(method_name, *args, &block)
39
48
  end
40
49
  end
41
50
  end
42
- end
43
-
44
- class AttributeDefinition
45
- def initialize(attributes)
46
- @attributes = attributes.map(&:to_s)
47
- end
48
51
 
49
52
  def matches?(method_name)
50
- attr_name = convert_to_reader_name(method_name)
51
- attributes.include?(attr_name)
53
+ attr_name = method_name.to_s.chomp("=")
54
+ attributes.any? { |name| name == attr_name }
52
55
  end
53
56
 
54
57
  def define_on(klass)
55
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
56
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
57
- klass.send(:attr_reader, *attr_readers)
58
- klass.send(:attr_writer, *attr_writers)
59
- end
58
+ @lock&.synchronize do
59
+ return unless @lock
60
60
 
61
- # TODO Change this to private once we've dropped Ruby 2.2 support.
62
- # Workaround for Ruby 2.2 "private attribute?" warning.
63
- protected
61
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
62
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
64
63
 
65
- attr_reader :attributes
64
+ attr_reader(*attr_readers)
65
+ attr_writer(*attr_writers)
66
66
 
67
- private
67
+ remove_method :respond_to_missing?
68
+ remove_method :method_missing
68
69
 
69
- def convert_to_reader_name(method_name)
70
- method_name.to_s.chomp("=")
70
+ @lock = nil
71
71
  end
72
+ end
73
+
74
+ def ==(other)
75
+ self.class == other.class && attributes == other.attributes
76
+ end
77
+
78
+ protected
79
+ attr_reader :attributes
72
80
  end
73
81
  end
74
82
 
@@ -32,7 +32,7 @@ module ActiveModel
32
32
  @delimiter ||= options[:in] || options[:within]
33
33
  end
34
34
 
35
- # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
35
+ # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
36
36
  # possible values in the range for equality, which is slower but more accurate.
37
37
  # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
38
38
  # endpoints, which is fast but is only accurate on Numeric, Time, Date,
@@ -19,11 +19,11 @@ module ActiveModel
19
19
 
20
20
  private
21
21
  def setup!(klass)
22
- klass.send(:attr_reader, *attributes.map do |attribute|
22
+ klass.attr_reader(*attributes.map do |attribute|
23
23
  :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
24
24
  end.compact)
25
25
 
26
- klass.send(:attr_writer, *attributes.map do |attribute|
26
+ klass.attr_writer(*attributes.map do |attribute|
27
27
  :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
28
28
  end.compact)
29
29
  end
@@ -19,7 +19,7 @@ module ActiveModel
19
19
  # particular enumerable object.
20
20
  #
21
21
  # class Person < ActiveRecord::Base
22
- # validates_inclusion_of :gender, in: %w( m f )
22
+ # validates_inclusion_of :role, in: %w( admin contributor )
23
23
  # validates_inclusion_of :age, in: 0..99
24
24
  # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
25
25
  # validates_inclusion_of :states, in: ->(person) { STATES[person.country] }