dynamoid 3.2.0 → 3.3.0
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 +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
|