activemodel 5.1.7 → 5.2.0.beta1

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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +32 -93
  3. data/README.rdoc +1 -1
  4. data/lib/active_model.rb +6 -1
  5. data/lib/active_model/attribute.rb +243 -0
  6. data/lib/active_model/attribute/user_provided_default.rb +30 -0
  7. data/lib/active_model/attribute_assignment.rb +8 -5
  8. data/lib/active_model/attribute_methods.rb +12 -10
  9. data/lib/active_model/attribute_mutation_tracker.rb +116 -0
  10. data/lib/active_model/attribute_set.rb +113 -0
  11. data/lib/active_model/attribute_set/builder.rb +124 -0
  12. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  13. data/lib/active_model/attributes.rb +108 -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 +124 -57
  17. data/lib/active_model/errors.rb +32 -21
  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 +2 -0
  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 +2 -0
  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.rb +6 -0
  29. data/lib/active_model/type/big_integer.rb +2 -0
  30. data/lib/active_model/type/binary.rb +2 -0
  31. data/lib/active_model/type/boolean.rb +2 -0
  32. data/lib/active_model/type/date.rb +2 -0
  33. data/lib/active_model/type/date_time.rb +6 -0
  34. data/lib/active_model/type/decimal.rb +2 -0
  35. data/lib/active_model/type/float.rb +2 -0
  36. data/lib/active_model/type/helpers.rb +2 -0
  37. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +6 -0
  38. data/lib/active_model/type/helpers/mutable.rb +2 -0
  39. data/lib/active_model/type/helpers/numeric.rb +2 -0
  40. data/lib/active_model/type/helpers/time_value.rb +2 -1
  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 +8 -4
  46. data/lib/active_model/type/value.rb +3 -1
  47. data/lib/active_model/validations.rb +7 -3
  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 +11 -13
  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 +3 -1
  59. data/lib/active_model/validations/presence.rb +1 -0
  60. data/lib/active_model/validations/validates.rb +4 -3
  61. data/lib/active_model/validations/with.rb +2 -0
  62. data/lib/active_model/validator.rb +6 -4
  63. data/lib/active_model/version.rb +2 -0
  64. metadata +17 -9
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/hash/keys"
2
4
 
3
5
  module ActiveModel
@@ -19,15 +21,15 @@ module ActiveModel
19
21
  # cat = Cat.new
20
22
  # cat.assign_attributes(name: "Gorby", status: "yawning")
21
23
  # cat.name # => 'Gorby'
22
- # cat.status => 'yawning'
24
+ # cat.status # => 'yawning'
23
25
  # cat.assign_attributes(status: "sleeping")
24
26
  # cat.name # => 'Gorby'
25
- # cat.status => 'sleeping'
27
+ # cat.status # => 'sleeping'
26
28
  def assign_attributes(new_attributes)
27
29
  if !new_attributes.respond_to?(:stringify_keys)
28
30
  raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
29
31
  end
30
- return if new_attributes.nil? || new_attributes.empty?
32
+ return if new_attributes.empty?
31
33
 
32
34
  attributes = new_attributes.stringify_keys
33
35
  _assign_attributes(sanitize_for_mass_assignment(attributes))
@@ -42,8 +44,9 @@ module ActiveModel
42
44
  end
43
45
 
44
46
  def _assign_attribute(k, v)
45
- if respond_to?("#{k}=")
46
- public_send("#{k}=", v)
47
+ setter = :"#{k}="
48
+ if respond_to?(setter)
49
+ public_send(setter, v)
47
50
  else
48
51
  raise UnknownAttributeError.new(self, k)
49
52
  end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
- require "mutex_m"
3
4
 
4
5
  module ActiveModel
5
6
  # Raised when an attribute is not defined.
@@ -68,9 +69,8 @@ module ActiveModel
68
69
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
69
70
 
70
71
  included do
71
- class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
72
- self.attribute_aliases = {}
73
- self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
72
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
73
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
74
74
  end
75
75
 
76
76
  module ClassMethods
@@ -328,13 +328,11 @@ module ActiveModel
328
328
  attribute_method_matchers_cache.clear
329
329
  end
330
330
 
331
- def generated_attribute_methods #:nodoc:
332
- @generated_attribute_methods ||= Module.new {
333
- extend Mutex_m
334
- }.tap { |mod| include mod }
335
- end
336
-
337
331
  private
332
+ def generated_attribute_methods
333
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
334
+ end
335
+
338
336
  def instance_method_already_implemented?(method_name)
339
337
  generated_attribute_methods.method_defined?(method_name)
340
338
  end
@@ -472,5 +470,9 @@ module ActiveModel
472
470
  def missing_attribute(attr_name, stack)
473
471
  raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
474
472
  end
473
+
474
+ def _read_attribute(attr)
475
+ __send__(attr)
476
+ end
475
477
  end
476
478
  end
@@ -0,0 +1,116 @@
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_values
15
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
16
+ if changed?(attr_name)
17
+ result[attr_name] = attributes[attr_name].original_value
18
+ end
19
+ end
20
+ end
21
+
22
+ def changes
23
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
24
+ change = change_to_attribute(attr_name)
25
+ if change
26
+ result[attr_name] = change
27
+ end
28
+ end
29
+ end
30
+
31
+ def change_to_attribute(attr_name)
32
+ attr_name = attr_name.to_s
33
+ if changed?(attr_name)
34
+ [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
35
+ end
36
+ end
37
+
38
+ def any_changes?
39
+ attr_names.any? { |attr| changed?(attr) }
40
+ end
41
+
42
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
43
+ attr_name = attr_name.to_s
44
+ forced_changes.include?(attr_name) ||
45
+ attributes[attr_name].changed? &&
46
+ (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
47
+ (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
48
+ end
49
+
50
+ def changed_in_place?(attr_name)
51
+ attributes[attr_name.to_s].changed_in_place?
52
+ end
53
+
54
+ def forget_change(attr_name)
55
+ attr_name = attr_name.to_s
56
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
57
+ forced_changes.delete(attr_name)
58
+ end
59
+
60
+ def original_value(attr_name)
61
+ attributes[attr_name.to_s].original_value
62
+ end
63
+
64
+ def force_change(attr_name)
65
+ forced_changes << attr_name.to_s
66
+ end
67
+
68
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
69
+ # Workaround for Ruby 2.2 "private attribute?" warning.
70
+ protected
71
+
72
+ attr_reader :attributes, :forced_changes
73
+
74
+ private
75
+
76
+ def attr_names
77
+ attributes.keys
78
+ end
79
+ end
80
+
81
+ class NullMutationTracker # :nodoc:
82
+ include Singleton
83
+
84
+ def changed_values(*)
85
+ {}
86
+ end
87
+
88
+ def changes(*)
89
+ {}
90
+ end
91
+
92
+ def change_to_attribute(attr_name)
93
+ end
94
+
95
+ def any_changes?(*)
96
+ false
97
+ end
98
+
99
+ def changed?(*)
100
+ false
101
+ end
102
+
103
+ def changed_in_place?(*)
104
+ false
105
+ end
106
+
107
+ def forget_change(*)
108
+ end
109
+
110
+ def original_value(*)
111
+ end
112
+
113
+ def force_change(*)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute_set/builder"
4
+ require "active_model/attribute_set/yaml_encoder"
5
+
6
+ module ActiveModel
7
+ class AttributeSet # :nodoc:
8
+ delegate :each_value, :fetch, to: :attributes
9
+
10
+ def initialize(attributes)
11
+ @attributes = attributes
12
+ end
13
+
14
+ def [](name)
15
+ attributes[name] || Attribute.null(name)
16
+ end
17
+
18
+ def []=(name, value)
19
+ attributes[name] = value
20
+ end
21
+
22
+ def values_before_type_cast
23
+ attributes.transform_values(&:value_before_type_cast)
24
+ end
25
+
26
+ def to_hash
27
+ initialized_attributes.transform_values(&:value)
28
+ end
29
+ alias_method :to_h, :to_hash
30
+
31
+ def key?(name)
32
+ attributes.key?(name) && self[name].initialized?
33
+ end
34
+
35
+ def keys
36
+ attributes.each_key.select { |name| self[name].initialized? }
37
+ end
38
+
39
+ if defined?(JRUBY_VERSION)
40
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
41
+ # https://github.com/jruby/jruby/pull/2562
42
+ def fetch_value(name, &block)
43
+ self[name].value(&block)
44
+ end
45
+ else
46
+ def fetch_value(name)
47
+ self[name].value { |n| yield n if block_given? }
48
+ end
49
+ end
50
+
51
+ def write_from_database(name, value)
52
+ attributes[name] = self[name].with_value_from_database(value)
53
+ end
54
+
55
+ def write_from_user(name, value)
56
+ attributes[name] = self[name].with_value_from_user(value)
57
+ end
58
+
59
+ def write_cast_value(name, value)
60
+ attributes[name] = self[name].with_cast_value(value)
61
+ end
62
+
63
+ def freeze
64
+ @attributes.freeze
65
+ super
66
+ end
67
+
68
+ def deep_dup
69
+ self.class.allocate.tap do |copy|
70
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
71
+ end
72
+ end
73
+
74
+ def initialize_dup(_)
75
+ @attributes = attributes.dup
76
+ super
77
+ end
78
+
79
+ def initialize_clone(_)
80
+ @attributes = attributes.clone
81
+ super
82
+ end
83
+
84
+ def reset(key)
85
+ if key?(key)
86
+ write_from_database(key, nil)
87
+ end
88
+ end
89
+
90
+ def accessed
91
+ attributes.select { |_, attr| attr.has_been_read? }.keys
92
+ end
93
+
94
+ def map(&block)
95
+ new_attributes = attributes.transform_values(&block)
96
+ AttributeSet.new(new_attributes)
97
+ end
98
+
99
+ def ==(other)
100
+ attributes == other.attributes
101
+ end
102
+
103
+ protected
104
+
105
+ attr_reader :attributes
106
+
107
+ private
108
+
109
+ def initialized_attributes
110
+ attributes.select { |_, attr| attr.initialized? }
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,124 @@
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, :always_initialized, :default
9
+
10
+ def initialize(types, always_initialized = nil, &default)
11
+ @types = types
12
+ @always_initialized = always_initialized
13
+ @default = default
14
+ end
15
+
16
+ def build_from_database(values = {}, additional_types = {})
17
+ if always_initialized && !values.key?(always_initialized)
18
+ values[always_initialized] = nil
19
+ end
20
+
21
+ attributes = LazyAttributeHash.new(types, values, additional_types, &default)
22
+ AttributeSet.new(attributes)
23
+ end
24
+ end
25
+ end
26
+
27
+ class LazyAttributeHash # :nodoc:
28
+ delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize
29
+
30
+ def initialize(types, values, additional_types, &default)
31
+ @types = types
32
+ @values = values
33
+ @additional_types = additional_types
34
+ @materialized = false
35
+ @delegate_hash = {}
36
+ @default = default || proc {}
37
+ end
38
+
39
+ def key?(key)
40
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
41
+ end
42
+
43
+ def [](key)
44
+ delegate_hash[key] || assign_default_value(key)
45
+ end
46
+
47
+ def []=(key, value)
48
+ if frozen?
49
+ raise RuntimeError, "Can't modify frozen hash"
50
+ end
51
+ delegate_hash[key] = value
52
+ end
53
+
54
+ def deep_dup
55
+ dup.tap do |copy|
56
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
57
+ end
58
+ end
59
+
60
+ def initialize_dup(_)
61
+ @delegate_hash = Hash[delegate_hash]
62
+ super
63
+ end
64
+
65
+ def select
66
+ keys = types.keys | values.keys | delegate_hash.keys
67
+ keys.each_with_object({}) do |key, hash|
68
+ attribute = self[key]
69
+ if yield(key, attribute)
70
+ hash[key] = attribute
71
+ end
72
+ end
73
+ end
74
+
75
+ def ==(other)
76
+ if other.is_a?(LazyAttributeHash)
77
+ materialize == other.materialize
78
+ else
79
+ materialize == other
80
+ end
81
+ end
82
+
83
+ def marshal_dump
84
+ materialize
85
+ end
86
+
87
+ def marshal_load(delegate_hash)
88
+ @delegate_hash = delegate_hash
89
+ @types = {}
90
+ @values = {}
91
+ @additional_types = {}
92
+ @materialized = true
93
+ end
94
+
95
+ protected
96
+
97
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default
98
+
99
+ def materialize
100
+ unless @materialized
101
+ values.each_key { |key| self[key] }
102
+ types.each_key { |key| self[key] }
103
+ unless frozen?
104
+ @materialized = true
105
+ end
106
+ end
107
+ delegate_hash
108
+ end
109
+
110
+ private
111
+
112
+ def assign_default_value(name)
113
+ type = additional_types.fetch(name, types[name])
114
+ value_present = true
115
+ value = values.fetch(name) { value_present = false }
116
+
117
+ if value_present
118
+ delegate_hash[name] = Attribute.from_database(name, value, type)
119
+ elsif types.key?(name)
120
+ delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
121
+ end
122
+ end
123
+ end
124
+ 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