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.
- 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
|