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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +32 -93
- data/README.rdoc +1 -1
- data/lib/active_model.rb +6 -1
- data/lib/active_model/attribute.rb +243 -0
- data/lib/active_model/attribute/user_provided_default.rb +30 -0
- data/lib/active_model/attribute_assignment.rb +8 -5
- data/lib/active_model/attribute_methods.rb +12 -10
- data/lib/active_model/attribute_mutation_tracker.rb +116 -0
- data/lib/active_model/attribute_set.rb +113 -0
- data/lib/active_model/attribute_set/builder.rb +124 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
- data/lib/active_model/attributes.rb +108 -0
- data/lib/active_model/callbacks.rb +7 -2
- data/lib/active_model/conversion.rb +2 -0
- data/lib/active_model/dirty.rb +124 -57
- data/lib/active_model/errors.rb +32 -21
- data/lib/active_model/forbidden_attributes_protection.rb +2 -0
- data/lib/active_model/gem_version.rb +5 -3
- data/lib/active_model/lint.rb +2 -0
- data/lib/active_model/model.rb +2 -0
- data/lib/active_model/naming.rb +5 -3
- data/lib/active_model/railtie.rb +2 -0
- data/lib/active_model/secure_password.rb +5 -3
- data/lib/active_model/serialization.rb +2 -0
- data/lib/active_model/serializers/json.rb +3 -2
- data/lib/active_model/translation.rb +2 -0
- data/lib/active_model/type.rb +6 -0
- data/lib/active_model/type/big_integer.rb +2 -0
- data/lib/active_model/type/binary.rb +2 -0
- data/lib/active_model/type/boolean.rb +2 -0
- data/lib/active_model/type/date.rb +2 -0
- data/lib/active_model/type/date_time.rb +6 -0
- data/lib/active_model/type/decimal.rb +2 -0
- data/lib/active_model/type/float.rb +2 -0
- data/lib/active_model/type/helpers.rb +2 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +6 -0
- data/lib/active_model/type/helpers/mutable.rb +2 -0
- data/lib/active_model/type/helpers/numeric.rb +2 -0
- data/lib/active_model/type/helpers/time_value.rb +2 -1
- data/lib/active_model/type/immutable_string.rb +2 -0
- data/lib/active_model/type/integer.rb +3 -1
- data/lib/active_model/type/registry.rb +2 -0
- data/lib/active_model/type/string.rb +2 -0
- data/lib/active_model/type/time.rb +8 -4
- data/lib/active_model/type/value.rb +3 -1
- data/lib/active_model/validations.rb +7 -3
- data/lib/active_model/validations/absence.rb +2 -0
- data/lib/active_model/validations/acceptance.rb +2 -0
- data/lib/active_model/validations/callbacks.rb +11 -13
- data/lib/active_model/validations/clusivity.rb +2 -0
- data/lib/active_model/validations/confirmation.rb +3 -1
- data/lib/active_model/validations/exclusion.rb +2 -0
- data/lib/active_model/validations/format.rb +1 -0
- data/lib/active_model/validations/helper_methods.rb +2 -0
- data/lib/active_model/validations/inclusion.rb +2 -0
- data/lib/active_model/validations/length.rb +10 -2
- data/lib/active_model/validations/numericality.rb +3 -1
- data/lib/active_model/validations/presence.rb +1 -0
- data/lib/active_model/validations/validates.rb +4 -3
- data/lib/active_model/validations/with.rb +2 -0
- data/lib/active_model/validator.rb +6 -4
- data/lib/active_model/version.rb +2 -0
- 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.
|
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
|
-
|
46
|
-
|
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, :
|
72
|
-
|
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
|