dynamoid 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +9 -6
- data/Appraisals +8 -14
- data/CHANGELOG.md +24 -0
- data/README.md +493 -228
- data/gemfiles/rails_4_2.gemfile +5 -7
- data/gemfiles/rails_5_0.gemfile +4 -6
- data/gemfiles/rails_5_1.gemfile +4 -6
- data/gemfiles/rails_5_2.gemfile +4 -6
- data/gemfiles/rails_6_0.gemfile +8 -0
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +3 -10
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +25 -69
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +105 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +9 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +11 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +11 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +3 -2
- data/lib/dynamoid/components.rb +6 -3
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +33 -6
- data/lib/dynamoid/criteria/key_fields_detector.rb +101 -32
- data/lib/dynamoid/dirty.rb +186 -34
- data/lib/dynamoid/document.rb +8 -216
- data/lib/dynamoid/fields.rb +8 -0
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/persistence.rb +177 -85
- data/lib/dynamoid/persistence/import.rb +72 -0
- data/lib/dynamoid/persistence/save.rb +63 -0
- data/lib/dynamoid/persistence/update_fields.rb +62 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +9 -2
@@ -4,9 +4,10 @@ module Dynamoid
|
|
4
4
|
module AdapterPlugin
|
5
5
|
class AwsSdkV3
|
6
6
|
class UntilPastTableStatus
|
7
|
-
attr_reader :table_name, :status
|
7
|
+
attr_reader :client, :table_name, :status
|
8
8
|
|
9
|
-
def initialize(table_name, status = :creating)
|
9
|
+
def initialize(client, table_name, status = :creating)
|
10
|
+
@client = client
|
10
11
|
@table_name = table_name
|
11
12
|
@status = status
|
12
13
|
end
|
data/lib/dynamoid/components.rb
CHANGED
@@ -17,21 +17,24 @@ module Dynamoid
|
|
17
17
|
after_initialize :set_inheritance_field
|
18
18
|
end
|
19
19
|
|
20
|
-
include ActiveModel::AttributeMethods
|
20
|
+
include ActiveModel::AttributeMethods # Actually it will be inclided in Dirty module again
|
21
21
|
include ActiveModel::Conversion
|
22
22
|
include ActiveModel::MassAssignmentSecurity if defined?(ActiveModel::MassAssignmentSecurity)
|
23
23
|
include ActiveModel::Naming
|
24
24
|
include ActiveModel::Observing if defined?(ActiveModel::Observing)
|
25
25
|
include ActiveModel::Serializers::JSON
|
26
26
|
include ActiveModel::Serializers::Xml if defined?(ActiveModel::Serializers::Xml)
|
27
|
+
include Dynamoid::Persistence
|
28
|
+
include Dynamoid::Loadable
|
29
|
+
# Dirty module should be included after Persistence and Loadable
|
30
|
+
# because it overrides some methods declared in these modules
|
31
|
+
include Dynamoid::Dirty
|
27
32
|
include Dynamoid::Fields
|
28
33
|
include Dynamoid::Indexes
|
29
|
-
include Dynamoid::Persistence
|
30
34
|
include Dynamoid::Finders
|
31
35
|
include Dynamoid::Associations
|
32
36
|
include Dynamoid::Criteria
|
33
37
|
include Dynamoid::Validations
|
34
38
|
include Dynamoid::IdentityMap
|
35
|
-
include Dynamoid::Dirty
|
36
39
|
end
|
37
40
|
end
|
data/lib/dynamoid/config.rb
CHANGED
@@ -31,6 +31,7 @@ module Dynamoid
|
|
31
31
|
option :sync_retry_max_times, default: 60 # a bit over 2 minutes
|
32
32
|
option :sync_retry_wait_seconds, default: 2
|
33
33
|
option :convert_big_decimal, default: false
|
34
|
+
option :store_attribute_with_nil_value, default: false # keep or ignore attribute with nil value at saving
|
34
35
|
option :models_dir, default: './app/models' # perhaps you keep your dynamoid models in a different directory?
|
35
36
|
option :application_timezone, default: :utc # available values - :utc, :local, time zone name like "Hawaii"
|
36
37
|
option :dynamodb_timezone, default: :utc # available values - :utc, :local, time zone name like "Hawaii"
|
data/lib/dynamoid/criteria.rb
CHANGED
@@ -8,7 +8,7 @@ module Dynamoid
|
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
-
%i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages].each do |meth|
|
11
|
+
%i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages project].each do |meth|
|
12
12
|
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
13
13
|
# see Dynamoid::Criteria::Chain.
|
14
14
|
#
|
@@ -159,6 +159,11 @@ module Dynamoid
|
|
159
159
|
pages.each(&block)
|
160
160
|
end
|
161
161
|
|
162
|
+
def project(*fields)
|
163
|
+
@project = fields.map(&:to_sym)
|
164
|
+
self
|
165
|
+
end
|
166
|
+
|
162
167
|
private
|
163
168
|
|
164
169
|
# The actual records referenced by the association.
|
@@ -190,9 +195,12 @@ module Dynamoid
|
|
190
195
|
#
|
191
196
|
# @since 3.1.0
|
192
197
|
def pages_via_query
|
193
|
-
Enumerator.new do |
|
198
|
+
Enumerator.new do |y|
|
194
199
|
Dynamoid.adapter.query(source.table_name, range_query).each do |items, metadata|
|
195
|
-
|
200
|
+
page = items.map { |h| source.from_database(h) }
|
201
|
+
options = metadata.slice(:last_evaluated_key)
|
202
|
+
|
203
|
+
y.yield page, options
|
196
204
|
end
|
197
205
|
end
|
198
206
|
end
|
@@ -203,9 +211,12 @@ module Dynamoid
|
|
203
211
|
#
|
204
212
|
# @since 3.1.0
|
205
213
|
def pages_via_scan
|
206
|
-
Enumerator.new do |
|
214
|
+
Enumerator.new do |y|
|
207
215
|
Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |items, metadata|
|
208
|
-
|
216
|
+
page = items.map { |h| source.from_database(h) }
|
217
|
+
options = metadata.slice(:last_evaluated_key)
|
218
|
+
|
219
|
+
y.yield page, options
|
209
220
|
end
|
210
221
|
end
|
211
222
|
end
|
@@ -271,6 +282,13 @@ module Dynamoid
|
|
271
282
|
{ contains: val }
|
272
283
|
when 'not_contains'
|
273
284
|
{ not_contains: val }
|
285
|
+
# NULL/NOT_NULL operators don't have parameters
|
286
|
+
# So { null: true } means NULL check and { null: false } means NOT_NULL one
|
287
|
+
# The same logic is used for { not_null: BOOL }
|
288
|
+
when 'null'
|
289
|
+
val ? { null: nil } : { not_null: nil }
|
290
|
+
when 'not_null'
|
291
|
+
val ? { not_null: nil } : { null: nil }
|
274
292
|
end
|
275
293
|
|
276
294
|
{ name.to_sym => hash }
|
@@ -314,10 +332,15 @@ module Dynamoid
|
|
314
332
|
opts.merge(query_opts).merge(consistent_opts)
|
315
333
|
end
|
316
334
|
|
335
|
+
# TODO casting should be operator aware
|
336
|
+
# e.g. for NULL operator value should be boolean
|
337
|
+
# and isn't related to an attribute own type
|
317
338
|
def type_cast_condition_parameter(key, value)
|
318
339
|
return value if %i[array set].include?(source.attributes[key.to_sym][:type])
|
319
340
|
|
320
|
-
if
|
341
|
+
if [true, false].include?(value) # Support argument for null/not_null operators
|
342
|
+
value
|
343
|
+
elsif !value.respond_to?(:to_ary)
|
321
344
|
options = source.attributes[key.to_sym]
|
322
345
|
value_casted = TypeCasting.cast_field(value, options)
|
323
346
|
Dumping.dump_field(value_casted, options)
|
@@ -356,13 +379,16 @@ module Dynamoid
|
|
356
379
|
|
357
380
|
def query_opts
|
358
381
|
opts = {}
|
382
|
+
# Don't specify select = ALL_ATTRIBUTES option explicitly because it's
|
383
|
+
# already a default value of Select statement. Explicite Select value
|
384
|
+
# conflicts with AttributesToGet statement (project option).
|
359
385
|
opts[:index_name] = @key_fields_detector.index_name if @key_fields_detector.index_name
|
360
|
-
opts[:select] = 'ALL_ATTRIBUTES'
|
361
386
|
opts[:record_limit] = @record_limit if @record_limit
|
362
387
|
opts[:scan_limit] = @scan_limit if @scan_limit
|
363
388
|
opts[:batch_size] = @batch_size if @batch_size
|
364
389
|
opts[:exclusive_start_key] = start_key if @start
|
365
390
|
opts[:scan_index_forward] = @scan_index_forward
|
391
|
+
opts[:project] = @project
|
366
392
|
opts
|
367
393
|
end
|
368
394
|
|
@@ -386,6 +412,7 @@ module Dynamoid
|
|
386
412
|
opts[:batch_size] = @batch_size if @batch_size
|
387
413
|
opts[:exclusive_start_key] = start_key if @start
|
388
414
|
opts[:consistent_read] = true if @consistent_read
|
415
|
+
opts[:project] = @project
|
389
416
|
opts
|
390
417
|
end
|
391
418
|
end
|
@@ -3,57 +3,126 @@
|
|
3
3
|
module Dynamoid #:nodoc:
|
4
4
|
module Criteria
|
5
5
|
class KeyFieldsDetector
|
6
|
-
|
6
|
+
|
7
|
+
class Query
|
8
|
+
def initialize(query_hash)
|
9
|
+
@query_hash = query_hash
|
10
|
+
@fields_with_operator = query_hash.keys.map(&:to_s)
|
11
|
+
@fields = query_hash.keys.map(&:to_s).map { |s| s.split('.').first }
|
12
|
+
end
|
13
|
+
|
14
|
+
def contain?(field_name)
|
15
|
+
@fields.include?(field_name.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def contain_with_eq_operator?(field_name)
|
19
|
+
@fields_with_operator.include?(field_name.to_s)
|
20
|
+
end
|
21
|
+
end
|
7
22
|
|
8
23
|
def initialize(query, source)
|
9
24
|
@query = query
|
10
25
|
@source = source
|
11
|
-
|
12
|
-
|
26
|
+
@query = Query.new(query)
|
27
|
+
@result = find_keys_in_query
|
13
28
|
end
|
14
29
|
|
15
30
|
def key_present?
|
16
|
-
@
|
31
|
+
@result.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def hash_key
|
35
|
+
@result && @result[:hash_key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def range_key
|
39
|
+
@result && @result[:range_key]
|
40
|
+
end
|
41
|
+
|
42
|
+
def index_name
|
43
|
+
@result && @result[:index_name]
|
17
44
|
end
|
18
45
|
|
19
46
|
private
|
20
47
|
|
21
|
-
def
|
22
|
-
|
48
|
+
def find_keys_in_query
|
49
|
+
match_table_and_sort_key ||
|
50
|
+
match_local_secondary_index ||
|
51
|
+
match_global_secondary_index_and_sort_key ||
|
52
|
+
match_table ||
|
53
|
+
match_global_secondary_index
|
54
|
+
end
|
55
|
+
|
56
|
+
# Use table's default range key
|
57
|
+
def match_table_and_sort_key
|
58
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
59
|
+
return unless @source.range_key
|
23
60
|
|
24
|
-
|
25
|
-
|
26
|
-
|
61
|
+
if @query.contain?(@source.range_key)
|
62
|
+
{
|
63
|
+
hash_key: @source.hash_key,
|
64
|
+
range_key: @source.range_key
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
27
68
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
69
|
+
# See if can use any local secondary index range key
|
70
|
+
# Chooses the first LSI found that can be utilized for the query
|
71
|
+
def match_local_secondary_index
|
72
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
33
73
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
74
|
+
lsi = @source.local_secondary_indexes.values.find do |lsi|
|
75
|
+
@query.contain?(lsi.range_key)
|
76
|
+
end
|
77
|
+
|
78
|
+
if lsi.present?
|
79
|
+
{
|
80
|
+
hash_key: @source.hash_key,
|
81
|
+
range_key: lsi.range_key,
|
82
|
+
index_name: lsi.name,
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
38
86
|
|
39
|
-
|
40
|
-
|
41
|
-
|
87
|
+
# See if can use any global secondary index
|
88
|
+
# Chooses the first GSI found that can be utilized for the query
|
89
|
+
# GSI with range key involved into query conditions has higher priority
|
90
|
+
# But only do so if projects ALL attributes otherwise we won't
|
91
|
+
# get back full data
|
92
|
+
def match_global_secondary_index_and_sort_key
|
93
|
+
gsi = @source.global_secondary_indexes.values.find do |gsi|
|
94
|
+
@query.contain_with_eq_operator?(gsi.hash_key) && gsi.projected_attributes == :all &&
|
95
|
+
@query.contain?(gsi.range_key)
|
96
|
+
end
|
42
97
|
|
43
|
-
|
98
|
+
if gsi.present?
|
99
|
+
{
|
100
|
+
hash_key: gsi.hash_key,
|
101
|
+
range_key: gsi.range_key,
|
102
|
+
index_name: gsi.name,
|
103
|
+
}
|
44
104
|
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def match_table
|
108
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
45
109
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
110
|
+
{
|
111
|
+
hash_key: @source.hash_key,
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def match_global_secondary_index
|
116
|
+
gsi = @source.global_secondary_indexes.values.find do |gsi|
|
117
|
+
@query.contain_with_eq_operator?(gsi.hash_key) && gsi.projected_attributes == :all
|
118
|
+
end
|
53
119
|
|
54
|
-
|
55
|
-
|
56
|
-
|
120
|
+
if gsi.present?
|
121
|
+
{
|
122
|
+
hash_key: gsi.hash_key,
|
123
|
+
range_key: gsi.range_key,
|
124
|
+
index_name: gsi.name,
|
125
|
+
}
|
57
126
|
end
|
58
127
|
end
|
59
128
|
end
|
data/lib/dynamoid/dirty.rb
CHANGED
@@ -1,67 +1,219 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Dynamoid
|
4
|
+
# Support interface of Rails' ActiveModel::Dirty module
|
5
|
+
#
|
6
|
+
# The reason why not just include ActiveModel::Dirty -
|
7
|
+
# ActiveModel::Dirty conflicts either with @attributes or
|
8
|
+
# #attributes in different Rails versions.
|
9
|
+
#
|
10
|
+
# Separate implementation (or copy-pasting) is the best way to
|
11
|
+
# avoid endless monkey-patching
|
12
|
+
#
|
13
|
+
# Documentation:
|
14
|
+
# https://api.rubyonrails.org/v4.2/classes/ActiveModel/Dirty.html
|
4
15
|
module Dirty
|
5
16
|
extend ActiveSupport::Concern
|
6
|
-
include ActiveModel::
|
17
|
+
include ActiveModel::AttributeMethods
|
18
|
+
|
19
|
+
included do
|
20
|
+
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
|
21
|
+
attribute_method_suffix '_previously_changed?', '_previous_change'
|
22
|
+
attribute_method_affix prefix: 'restore_', suffix: '!'
|
23
|
+
end
|
7
24
|
|
8
25
|
module ClassMethods
|
26
|
+
def update_fields(*)
|
27
|
+
if model = super
|
28
|
+
model.send(:clear_changes_information)
|
29
|
+
end
|
30
|
+
model
|
31
|
+
end
|
32
|
+
|
33
|
+
def upsert(*)
|
34
|
+
if model = super
|
35
|
+
model.send(:clear_changes_information)
|
36
|
+
end
|
37
|
+
model
|
38
|
+
end
|
39
|
+
|
9
40
|
def from_database(*)
|
10
|
-
super.tap
|
41
|
+
super.tap do |m|
|
42
|
+
m.send(:clear_changes_information)
|
43
|
+
end
|
11
44
|
end
|
12
45
|
end
|
13
46
|
|
14
47
|
def save(*)
|
15
|
-
|
48
|
+
if status = super
|
49
|
+
changes_applied
|
50
|
+
end
|
51
|
+
status
|
16
52
|
end
|
17
53
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
54
|
+
def save!(*)
|
55
|
+
super.tap do
|
56
|
+
changes_applied
|
57
|
+
end
|
22
58
|
end
|
23
59
|
|
24
|
-
def
|
25
|
-
super.tap
|
60
|
+
def update(*)
|
61
|
+
super.tap do
|
62
|
+
clear_changes_information
|
63
|
+
end
|
26
64
|
end
|
27
65
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
unless result == false # failed validation; nil is OK.
|
32
|
-
@previously_changed = previous
|
33
|
-
clear_changes_information
|
34
|
-
end
|
66
|
+
def update!(*)
|
67
|
+
super.tap do
|
68
|
+
clear_changes_information
|
35
69
|
end
|
36
70
|
end
|
37
71
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
72
|
+
def reload(*)
|
73
|
+
super.tap do
|
74
|
+
clear_changes_information
|
75
|
+
end
|
41
76
|
end
|
42
77
|
|
43
|
-
|
78
|
+
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
|
79
|
+
#
|
80
|
+
# person.changed? # => false
|
81
|
+
# person.name = 'bob'
|
82
|
+
# person.changed? # => true
|
83
|
+
def changed?
|
84
|
+
changed_attributes.present?
|
85
|
+
end
|
44
86
|
|
45
|
-
|
46
|
-
|
87
|
+
# Returns an array with the name of the attributes with unsaved changes.
|
88
|
+
#
|
89
|
+
# person.changed # => []
|
90
|
+
# person.name = 'bob'
|
91
|
+
# person.changed # => ["name"]
|
92
|
+
def changed
|
93
|
+
changed_attributes.keys
|
47
94
|
end
|
48
95
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
96
|
+
# Returns a hash of changed attributes indicating their original
|
97
|
+
# and new values like <tt>attr => [original value, new value]</tt>.
|
98
|
+
#
|
99
|
+
# person.changes # => {}
|
100
|
+
# person.name = 'bob'
|
101
|
+
# person.changes # => { "name" => ["bill", "bob"] }
|
102
|
+
def changes
|
103
|
+
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
104
|
+
end
|
53
105
|
|
54
|
-
|
55
|
-
|
56
|
-
|
106
|
+
# Returns a hash of attributes that were changed before the model was saved.
|
107
|
+
#
|
108
|
+
# person.name # => "bob"
|
109
|
+
# person.name = 'robert'
|
110
|
+
# person.save
|
111
|
+
# person.previous_changes # => {"name" => ["bob", "robert"]}
|
112
|
+
def previous_changes
|
113
|
+
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
|
114
|
+
end
|
57
115
|
|
58
|
-
|
116
|
+
# Returns a hash of the attributes with unsaved changes indicating their original
|
117
|
+
# values like <tt>attr => original value</tt>.
|
118
|
+
#
|
119
|
+
# person.name # => "bob"
|
120
|
+
# person.name = 'robert'
|
121
|
+
# person.changed_attributes # => {"name" => "bob"}
|
122
|
+
def changed_attributes
|
123
|
+
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
59
124
|
end
|
60
125
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
126
|
+
# Handle <tt>*_changed?</tt> for +method_missing+.
|
127
|
+
def attribute_changed?(attr, options = {}) #:nodoc:
|
128
|
+
result = changes_include?(attr)
|
129
|
+
result &&= options[:to] == __send__(attr) if options.key?(:to)
|
130
|
+
result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
134
|
+
# Handle <tt>*_was</tt> for +method_missing+.
|
135
|
+
def attribute_was(attr) # :nodoc:
|
136
|
+
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Restore all previous data of the provided attributes.
|
140
|
+
def restore_attributes(attributes = changed)
|
141
|
+
attributes.each { |attr| restore_attribute! attr }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
|
145
|
+
def attribute_previously_changed?(attr) #:nodoc:
|
146
|
+
previous_changes_include?(attr)
|
65
147
|
end
|
148
|
+
|
149
|
+
# Handles <tt>*_previous_change</tt> for +method_missing+.
|
150
|
+
def attribute_previous_change(attr)
|
151
|
+
previous_changes[attr] if attribute_previously_changed?(attr)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def changes_include?(attr_name)
|
157
|
+
attributes_changed_by_setter.include?(attr_name)
|
158
|
+
end
|
159
|
+
alias attribute_changed_by_setter? changes_include?
|
160
|
+
|
161
|
+
# Removes current changes and makes them accessible through +previous_changes+.
|
162
|
+
def changes_applied # :doc:
|
163
|
+
@previously_changed = changes
|
164
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
165
|
+
end
|
166
|
+
|
167
|
+
# Clear all dirty data: current changes and previous changes.
|
168
|
+
def clear_changes_information # :doc:
|
169
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
170
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
171
|
+
end
|
172
|
+
|
173
|
+
# Handle <tt>*_change</tt> for +method_missing+.
|
174
|
+
def attribute_change(attr)
|
175
|
+
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
179
|
+
def attribute_will_change!(attr)
|
180
|
+
return if attribute_changed?(attr)
|
181
|
+
|
182
|
+
begin
|
183
|
+
value = __send__(attr)
|
184
|
+
value = value.duplicable? ? value.clone : value
|
185
|
+
rescue TypeError, NoMethodError
|
186
|
+
end
|
187
|
+
|
188
|
+
set_attribute_was(attr, value)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Handle <tt>restore_*!</tt> for +method_missing+.
|
192
|
+
def restore_attribute!(attr)
|
193
|
+
if attribute_changed?(attr)
|
194
|
+
__send__("#{attr}=", changed_attributes[attr])
|
195
|
+
clear_attribute_changes([attr])
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns +true+ if attr_name were changed before the model was saved,
|
200
|
+
# +false+ otherwise.
|
201
|
+
def previous_changes_include?(attr_name)
|
202
|
+
previous_changes.include?(attr_name)
|
203
|
+
end
|
204
|
+
|
205
|
+
# This is necessary because `changed_attributes` might be overridden in
|
206
|
+
# other implemntations (e.g. in `ActiveRecord`)
|
207
|
+
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
|
208
|
+
|
209
|
+
# Force an attribute to have a particular "before" value
|
210
|
+
def set_attribute_was(attr, old_value)
|
211
|
+
attributes_changed_by_setter[attr] = old_value
|
212
|
+
end
|
213
|
+
|
214
|
+
# Remove changes information for the provided attributes.
|
215
|
+
def clear_attribute_changes(attributes) # :doc:
|
216
|
+
attributes_changed_by_setter.except!(*attributes)
|
217
|
+
end
|
66
218
|
end
|
67
219
|
end
|