activemodel 5.1.7 → 5.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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