activemodel 5.1.7 → 5.2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -40
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_model/attribute/user_provided_default.rb +52 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +10 -5
  8. data/lib/active_model/attribute_methods.rb +12 -10
  9. data/lib/active_model/attribute_mutation_tracker.rb +124 -0
  10. data/lib/active_model/attribute_set/builder.rb +126 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  12. data/lib/active_model/attribute_set.rb +114 -0
  13. data/lib/active_model/attributes.rb +111 -0
  14. data/lib/active_model/callbacks.rb +7 -2
  15. data/lib/active_model/conversion.rb +2 -0
  16. data/lib/active_model/dirty.rb +128 -57
  17. data/lib/active_model/errors.rb +31 -20
  18. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  19. data/lib/active_model/gem_version.rb +5 -3
  20. data/lib/active_model/lint.rb +14 -12
  21. data/lib/active_model/model.rb +2 -0
  22. data/lib/active_model/naming.rb +5 -3
  23. data/lib/active_model/railtie.rb +2 -0
  24. data/lib/active_model/secure_password.rb +5 -3
  25. data/lib/active_model/serialization.rb +3 -1
  26. data/lib/active_model/serializers/json.rb +3 -2
  27. data/lib/active_model/translation.rb +2 -0
  28. data/lib/active_model/type/big_integer.rb +2 -0
  29. data/lib/active_model/type/binary.rb +2 -0
  30. data/lib/active_model/type/boolean.rb +16 -1
  31. data/lib/active_model/type/date.rb +5 -2
  32. data/lib/active_model/type/date_time.rb +7 -0
  33. data/lib/active_model/type/decimal.rb +2 -0
  34. data/lib/active_model/type/float.rb +2 -0
  35. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +6 -0
  36. data/lib/active_model/type/helpers/mutable.rb +2 -0
  37. data/lib/active_model/type/helpers/numeric.rb +3 -1
  38. data/lib/active_model/type/helpers/time_value.rb +2 -12
  39. data/lib/active_model/type/helpers/timezone.rb +19 -0
  40. data/lib/active_model/type/helpers.rb +3 -0
  41. data/lib/active_model/type/immutable_string.rb +2 -0
  42. data/lib/active_model/type/integer.rb +3 -1
  43. data/lib/active_model/type/registry.rb +2 -0
  44. data/lib/active_model/type/string.rb +2 -0
  45. data/lib/active_model/type/time.rb +7 -0
  46. data/lib/active_model/type/value.rb +6 -0
  47. data/lib/active_model/type.rb +7 -1
  48. data/lib/active_model/validations/absence.rb +2 -0
  49. data/lib/active_model/validations/acceptance.rb +2 -0
  50. data/lib/active_model/validations/callbacks.rb +12 -8
  51. data/lib/active_model/validations/clusivity.rb +2 -0
  52. data/lib/active_model/validations/confirmation.rb +3 -1
  53. data/lib/active_model/validations/exclusion.rb +2 -0
  54. data/lib/active_model/validations/format.rb +1 -0
  55. data/lib/active_model/validations/helper_methods.rb +2 -0
  56. data/lib/active_model/validations/inclusion.rb +2 -0
  57. data/lib/active_model/validations/length.rb +10 -2
  58. data/lib/active_model/validations/numericality.rb +39 -13
  59. data/lib/active_model/validations/presence.rb +1 -0
  60. data/lib/active_model/validations/validates.rb +5 -4
  61. data/lib/active_model/validations/with.rb +2 -0
  62. data/lib/active_model/validations.rb +10 -6
  63. data/lib/active_model/validator.rb +6 -4
  64. data/lib/active_model/version.rb +2 -0
  65. data/lib/active_model.rb +7 -2
  66. metadata +18 -10
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ module ActiveModel
6
+ class AttributeMutationTracker # :nodoc:
7
+ OPTION_NOT_GIVEN = Object.new
8
+
9
+ def initialize(attributes)
10
+ @attributes = attributes
11
+ @forced_changes = Set.new
12
+ end
13
+
14
+ def changed_attribute_names
15
+ attr_names.select { |attr_name| changed?(attr_name) }
16
+ end
17
+
18
+ def changed_values
19
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
20
+ if changed?(attr_name)
21
+ result[attr_name] = attributes[attr_name].original_value
22
+ end
23
+ end
24
+ end
25
+
26
+ def changes
27
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
28
+ change = change_to_attribute(attr_name)
29
+ if change
30
+ result.merge!(attr_name => change)
31
+ end
32
+ end
33
+ end
34
+
35
+ def change_to_attribute(attr_name)
36
+ attr_name = attr_name.to_s
37
+ if changed?(attr_name)
38
+ [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
39
+ end
40
+ end
41
+
42
+ def any_changes?
43
+ attr_names.any? { |attr| changed?(attr) }
44
+ end
45
+
46
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
47
+ attr_name = attr_name.to_s
48
+ forced_changes.include?(attr_name) ||
49
+ attributes[attr_name].changed? &&
50
+ (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
51
+ (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
52
+ end
53
+
54
+ def changed_in_place?(attr_name)
55
+ attributes[attr_name.to_s].changed_in_place?
56
+ end
57
+
58
+ def forget_change(attr_name)
59
+ attr_name = attr_name.to_s
60
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
61
+ forced_changes.delete(attr_name)
62
+ end
63
+
64
+ def original_value(attr_name)
65
+ attributes[attr_name.to_s].original_value
66
+ end
67
+
68
+ def force_change(attr_name)
69
+ forced_changes << attr_name.to_s
70
+ end
71
+
72
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
73
+ # Workaround for Ruby 2.2 "private attribute?" warning.
74
+ protected
75
+
76
+ attr_reader :attributes, :forced_changes
77
+
78
+ private
79
+
80
+ def attr_names
81
+ attributes.keys
82
+ end
83
+ end
84
+
85
+ class NullMutationTracker # :nodoc:
86
+ include Singleton
87
+
88
+ def changed_attribute_names(*)
89
+ []
90
+ end
91
+
92
+ def changed_values(*)
93
+ {}
94
+ end
95
+
96
+ def changes(*)
97
+ {}
98
+ end
99
+
100
+ def change_to_attribute(attr_name)
101
+ end
102
+
103
+ def any_changes?(*)
104
+ false
105
+ end
106
+
107
+ def changed?(*)
108
+ false
109
+ end
110
+
111
+ def changed_in_place?(*)
112
+ false
113
+ end
114
+
115
+ def forget_change(*)
116
+ end
117
+
118
+ def original_value(*)
119
+ end
120
+
121
+ def force_change(*)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute"
4
+
5
+ module ActiveModel
6
+ class AttributeSet # :nodoc:
7
+ class Builder # :nodoc:
8
+ attr_reader :types, :default_attributes
9
+
10
+ def initialize(types, default_attributes = {})
11
+ @types = types
12
+ @default_attributes = default_attributes
13
+ end
14
+
15
+ def build_from_database(values = {}, additional_types = {})
16
+ attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
17
+ AttributeSet.new(attributes)
18
+ end
19
+ end
20
+ end
21
+
22
+ class LazyAttributeHash # :nodoc:
23
+ delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
24
+
25
+ def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
26
+ @types = types
27
+ @values = values
28
+ @additional_types = additional_types
29
+ @materialized = false
30
+ @delegate_hash = delegate_hash
31
+ @default_attributes = default_attributes
32
+ end
33
+
34
+ def key?(key)
35
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
36
+ end
37
+
38
+ def [](key)
39
+ delegate_hash[key] || assign_default_value(key)
40
+ end
41
+
42
+ def []=(key, value)
43
+ if frozen?
44
+ raise RuntimeError, "Can't modify frozen hash"
45
+ end
46
+ delegate_hash[key] = value
47
+ end
48
+
49
+ def deep_dup
50
+ dup.tap do |copy|
51
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
52
+ end
53
+ end
54
+
55
+ def initialize_dup(_)
56
+ @delegate_hash = Hash[delegate_hash]
57
+ super
58
+ end
59
+
60
+ def select
61
+ keys = types.keys | values.keys | delegate_hash.keys
62
+ keys.each_with_object({}) do |key, hash|
63
+ attribute = self[key]
64
+ if yield(key, attribute)
65
+ hash[key] = attribute
66
+ end
67
+ end
68
+ end
69
+
70
+ def ==(other)
71
+ if other.is_a?(LazyAttributeHash)
72
+ materialize == other.materialize
73
+ else
74
+ materialize == other
75
+ end
76
+ end
77
+
78
+ def marshal_dump
79
+ [@types, @values, @additional_types, @default_attributes, @delegate_hash]
80
+ end
81
+
82
+ def marshal_load(values)
83
+ if values.is_a?(Hash)
84
+ empty_hash = {}.freeze
85
+ initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
86
+ @materialized = true
87
+ else
88
+ initialize(*values)
89
+ end
90
+ end
91
+
92
+ protected
93
+
94
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
95
+
96
+ def materialize
97
+ unless @materialized
98
+ values.each_key { |key| self[key] }
99
+ types.each_key { |key| self[key] }
100
+ unless frozen?
101
+ @materialized = true
102
+ end
103
+ end
104
+ delegate_hash
105
+ end
106
+
107
+ private
108
+
109
+ def assign_default_value(name)
110
+ type = additional_types.fetch(name, types[name])
111
+ value_present = true
112
+ value = values.fetch(name) { value_present = false }
113
+
114
+ if value_present
115
+ delegate_hash[name] = Attribute.from_database(name, value, type)
116
+ elsif types.key?(name)
117
+ attr = default_attributes[name]
118
+ if attr
119
+ delegate_hash[name] = attr.dup
120
+ else
121
+ delegate_hash[name] = Attribute.uninitialized(name, type)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class AttributeSet
5
+ # Attempts to do more intelligent YAML dumping of an
6
+ # ActiveModel::AttributeSet to reduce the size of the resulting string
7
+ class YAMLEncoder # :nodoc:
8
+ def initialize(default_types)
9
+ @default_types = default_types
10
+ end
11
+
12
+ def encode(attribute_set, coder)
13
+ coder["concise_attributes"] = attribute_set.each_value.map do |attr|
14
+ if attr.type.equal?(default_types[attr.name])
15
+ attr.with_type(nil)
16
+ else
17
+ attr
18
+ end
19
+ end
20
+ end
21
+
22
+ def decode(coder)
23
+ if coder["attributes"]
24
+ coder["attributes"]
25
+ else
26
+ attributes_hash = Hash[coder["concise_attributes"].map do |attr|
27
+ if attr.type.nil?
28
+ attr = attr.with_type(default_types[attr.name])
29
+ end
30
+ [attr.name, attr]
31
+ end]
32
+ AttributeSet.new(attributes_hash)
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :default_types
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+ require "active_model/attribute_set/builder"
5
+ require "active_model/attribute_set/yaml_encoder"
6
+
7
+ module ActiveModel
8
+ class AttributeSet # :nodoc:
9
+ delegate :each_value, :fetch, :except, to: :attributes
10
+
11
+ def initialize(attributes)
12
+ @attributes = attributes
13
+ end
14
+
15
+ def [](name)
16
+ attributes[name] || Attribute.null(name)
17
+ end
18
+
19
+ def []=(name, value)
20
+ attributes[name] = value
21
+ end
22
+
23
+ def values_before_type_cast
24
+ attributes.transform_values(&:value_before_type_cast)
25
+ end
26
+
27
+ def to_hash
28
+ initialized_attributes.transform_values(&:value)
29
+ end
30
+ alias_method :to_h, :to_hash
31
+
32
+ def key?(name)
33
+ attributes.key?(name) && self[name].initialized?
34
+ end
35
+
36
+ def keys
37
+ attributes.each_key.select { |name| self[name].initialized? }
38
+ end
39
+
40
+ if defined?(JRUBY_VERSION)
41
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
42
+ # https://github.com/jruby/jruby/pull/2562
43
+ def fetch_value(name, &block)
44
+ self[name].value(&block)
45
+ end
46
+ else
47
+ def fetch_value(name)
48
+ self[name].value { |n| yield n if block_given? }
49
+ end
50
+ end
51
+
52
+ def write_from_database(name, value)
53
+ attributes[name] = self[name].with_value_from_database(value)
54
+ end
55
+
56
+ def write_from_user(name, value)
57
+ attributes[name] = self[name].with_value_from_user(value)
58
+ end
59
+
60
+ def write_cast_value(name, value)
61
+ attributes[name] = self[name].with_cast_value(value)
62
+ end
63
+
64
+ def freeze
65
+ @attributes.freeze
66
+ super
67
+ end
68
+
69
+ def deep_dup
70
+ self.class.allocate.tap do |copy|
71
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
72
+ end
73
+ end
74
+
75
+ def initialize_dup(_)
76
+ @attributes = attributes.dup
77
+ super
78
+ end
79
+
80
+ def initialize_clone(_)
81
+ @attributes = attributes.clone
82
+ super
83
+ end
84
+
85
+ def reset(key)
86
+ if key?(key)
87
+ write_from_database(key, nil)
88
+ end
89
+ end
90
+
91
+ def accessed
92
+ attributes.select { |_, attr| attr.has_been_read? }.keys
93
+ end
94
+
95
+ def map(&block)
96
+ new_attributes = attributes.transform_values(&block)
97
+ AttributeSet.new(new_attributes)
98
+ end
99
+
100
+ def ==(other)
101
+ attributes == other.attributes
102
+ end
103
+
104
+ protected
105
+
106
+ attr_reader :attributes
107
+
108
+ private
109
+
110
+ def initialized_attributes
111
+ attributes.select { |_, attr| attr.initialized? }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute_set"
4
+ require "active_model/attribute/user_provided_default"
5
+
6
+ module ActiveModel
7
+ module Attributes #:nodoc:
8
+ extend ActiveSupport::Concern
9
+ include ActiveModel::AttributeMethods
10
+
11
+ included do
12
+ attribute_method_suffix "="
13
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
14
+ self.attribute_types = Hash.new(Type.default_value)
15
+ self._default_attributes = AttributeSet.new({})
16
+ end
17
+
18
+ module ClassMethods
19
+ def attribute(name, type = Type::Value.new, **options)
20
+ name = name.to_s
21
+ if type.is_a?(Symbol)
22
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
23
+ end
24
+ self.attribute_types = attribute_types.merge(name => type)
25
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
26
+ define_attribute_method(name)
27
+ end
28
+
29
+ private
30
+
31
+ def define_method_attribute=(name)
32
+ safe_name = name.unpack("h*".freeze).first
33
+ ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
34
+
35
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
36
+ def __temp__#{safe_name}=(value)
37
+ name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
38
+ write_attribute(name, value)
39
+ end
40
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
41
+ undef_method :__temp__#{safe_name}=
42
+ STR
43
+ end
44
+
45
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
46
+ private_constant :NO_DEFAULT_PROVIDED
47
+
48
+ def define_default_attribute(name, value, type)
49
+ self._default_attributes = _default_attributes.deep_dup
50
+ if value == NO_DEFAULT_PROVIDED
51
+ default_attribute = _default_attributes[name].with_type(type)
52
+ else
53
+ default_attribute = Attribute::UserProvidedDefault.new(
54
+ name,
55
+ value,
56
+ type,
57
+ _default_attributes.fetch(name.to_s) { nil },
58
+ )
59
+ end
60
+ _default_attributes[name] = default_attribute
61
+ end
62
+ end
63
+
64
+ def initialize(*)
65
+ @attributes = self.class._default_attributes.deep_dup
66
+ super
67
+ end
68
+
69
+ def attributes
70
+ @attributes.to_hash
71
+ end
72
+
73
+ private
74
+
75
+ def write_attribute(attr_name, value)
76
+ name = if self.class.attribute_alias?(attr_name)
77
+ self.class.attribute_alias(attr_name).to_s
78
+ else
79
+ attr_name.to_s
80
+ end
81
+
82
+ @attributes.write_from_user(name, value)
83
+ value
84
+ end
85
+
86
+ def attribute(attr_name)
87
+ name = if self.class.attribute_alias?(attr_name)
88
+ self.class.attribute_alias(attr_name).to_s
89
+ else
90
+ attr_name.to_s
91
+ end
92
+ @attributes.fetch_value(name)
93
+ end
94
+
95
+ # Handle *= for method_missing.
96
+ def attribute=(attribute_name, value)
97
+ write_attribute(attribute_name, value)
98
+ end
99
+ end
100
+
101
+ module AttributeMethods #:nodoc:
102
+ AttrNames = Module.new {
103
+ def self.set_name_cache(name, value)
104
+ const_name = "ATTR_#{name}"
105
+ unless const_defined? const_name
106
+ const_set const_name, value.dup.freeze
107
+ end
108
+ end
109
+ }
110
+ end
111
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/array/extract_options"
2
4
 
3
5
  module ActiveModel
@@ -56,6 +58,9 @@ module ActiveModel
56
58
  #
57
59
  # Would only create the +after_create+ and +before_create+ callback methods in
58
60
  # your class.
61
+ #
62
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
63
+ #
59
64
  module Callbacks
60
65
  def self.extended(base) #:nodoc:
61
66
  base.class_eval do
@@ -98,8 +103,8 @@ module ActiveModel
98
103
  # end
99
104
  # end
100
105
  #
101
- # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
102
- # `!`, `?` or `=`.
106
+ # NOTE: +method_name+ passed to define_model_callbacks must not end with
107
+ # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
103
108
  def define_model_callbacks(*callbacks)
104
109
  options = callbacks.extract_options!
105
110
  options = {
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveModel
2
4
  # == Active \Model \Conversion
3
5
  #