dynamoid 3.9.0 → 3.11.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/README.md +202 -25
  4. data/dynamoid.gemspec +5 -6
  5. data/lib/dynamoid/adapter.rb +19 -13
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
  14. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  15. data/lib/dynamoid/associations.rb +1 -1
  16. data/lib/dynamoid/components.rb +1 -0
  17. data/lib/dynamoid/config/options.rb +12 -12
  18. data/lib/dynamoid/config.rb +3 -0
  19. data/lib/dynamoid/criteria/chain.rb +149 -142
  20. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  21. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  22. data/lib/dynamoid/criteria/where_conditions.rb +36 -0
  23. data/lib/dynamoid/dirty.rb +87 -12
  24. data/lib/dynamoid/document.rb +1 -1
  25. data/lib/dynamoid/dumping.rb +38 -16
  26. data/lib/dynamoid/errors.rb +14 -2
  27. data/lib/dynamoid/fields/declare.rb +6 -6
  28. data/lib/dynamoid/fields.rb +6 -8
  29. data/lib/dynamoid/finders.rb +23 -32
  30. data/lib/dynamoid/indexes.rb +6 -7
  31. data/lib/dynamoid/loadable.rb +3 -2
  32. data/lib/dynamoid/persistence/inc.rb +6 -7
  33. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  34. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  35. data/lib/dynamoid/persistence/save.rb +17 -18
  36. data/lib/dynamoid/persistence/update_fields.rb +7 -5
  37. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  38. data/lib/dynamoid/persistence/upsert.rb +5 -4
  39. data/lib/dynamoid/persistence.rb +77 -21
  40. data/lib/dynamoid/transaction_write/base.rb +47 -0
  41. data/lib/dynamoid/transaction_write/create.rb +49 -0
  42. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  43. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  44. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  45. data/lib/dynamoid/transaction_write/save.rb +164 -0
  46. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  47. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  48. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  49. data/lib/dynamoid/transaction_write.rb +464 -0
  50. data/lib/dynamoid/type_casting.rb +18 -15
  51. data/lib/dynamoid/undumping.rb +14 -3
  52. data/lib/dynamoid/validations.rb +1 -1
  53. data/lib/dynamoid/version.rb +1 -1
  54. data/lib/dynamoid.rb +7 -0
  55. metadata +30 -16
  56. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  57. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid/transaction_write/create'
4
+ require 'dynamoid/transaction_write/delete_with_primary_key'
5
+ require 'dynamoid/transaction_write/delete_with_instance'
6
+ require 'dynamoid/transaction_write/destroy'
7
+ require 'dynamoid/transaction_write/save'
8
+ require 'dynamoid/transaction_write/update_fields'
9
+ require 'dynamoid/transaction_write/update_attributes'
10
+ require 'dynamoid/transaction_write/upsert'
11
+
12
+ module Dynamoid
13
+ # The class +TransactionWrite+ provides means to perform multiple modifying
14
+ # operations in transaction, that is atomically, so that either all of them
15
+ # succeed, or all of them fail.
16
+ #
17
+ # The persisting methods are supposed to be as close as possible to their
18
+ # non-transactional counterparts like +.create+, +#save+ and +#delete+:
19
+ #
20
+ # user = User.new()
21
+ # payment = Payment.find(1)
22
+ #
23
+ # Dynamoid::TransactionWrite.execute do |t|
24
+ # t.save! user
25
+ # t.create! Account, name: 'A'
26
+ # t.delete payment
27
+ # end
28
+ #
29
+ # The only difference is that the methods are called on a transaction
30
+ # instance and a model or a model class should be specified.
31
+ #
32
+ # So +user.save!+ becomes +t.save!(user)+, +Account.create!(name: 'A')+
33
+ # becomes +t.create!(Account, name: 'A')+, and +payment.delete+ becomes
34
+ # +t.delete(payment)+.
35
+ #
36
+ # A transaction can be used without a block. This way a transaction instance
37
+ # should be instantiated and committed manually with +#commit+ method:
38
+ #
39
+ # t = Dynamoid::TransactionWrite.new
40
+ #
41
+ # t.save! user
42
+ # t.create! Account, name: 'A'
43
+ # t.delete payment
44
+ #
45
+ # t.commit
46
+ #
47
+ # Some persisting methods are intentionally not available in a transaction,
48
+ # e.g. +.update+ and +.update!+ that simply call +.find+ and
49
+ # +#update_attributes+ methods. These methods perform multiple operations so
50
+ # cannot be implemented in a transactional atomic way.
51
+ #
52
+ #
53
+ # ### DynamoDB's transactions
54
+ #
55
+ # The main difference between DynamoDB transactions and a common interface is
56
+ # that DynamoDB's transactions are executed in batch. So in Dynamoid no
57
+ # changes are actually persisted when some transactional method (e.g+ `#save+) is
58
+ # called. All the changes are persisted at the end.
59
+ #
60
+ # A +TransactWriteItems+ DynamoDB operation is used (see
61
+ # [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html)
62
+ # for details).
63
+ #
64
+ #
65
+ # ### Callbacks
66
+ #
67
+ # The transactional methods support +before_+, +after_+ and +around_+
68
+ # callbacks to the extend the non-transactional methods support them.
69
+ #
70
+ # There is important difference - a transactional method runs callbacks
71
+ # immediately (even +after_+ ones) when it is called before changes are
72
+ # actually persisted. So code in +after_+ callbacks does not see observes
73
+ # them in DynamoDB and so for.
74
+ #
75
+ # When a callback aborts persisting of a model or a model is invalid then
76
+ # transaction is not aborted and may commit successfully.
77
+ #
78
+ #
79
+ # ### Transaction rollback
80
+ #
81
+ # A transaction is rolled back on DynamoDB's side automatically when:
82
+ # - an ongoing operation is in the process of updating the same item.
83
+ # - there is insufficient provisioned capacity for the transaction to be completed.
84
+ # - an item size becomes too large (bigger than 400 KB), a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
85
+ # - the aggregate size of the items in the transaction exceeds 4 MB.
86
+ # - there is a user error, such as an invalid data format.
87
+ #
88
+ # A transaction can be interrupted simply by an exception raised within a
89
+ # block. As far as no changes are actually persisted before the +#commit+
90
+ # method call - there is nothing to undo on the DynamoDB's site.
91
+ #
92
+ # Raising +Dynamoid::Errors::Rollback+ exception leads to interrupting a
93
+ # transation and it isn't propogated:
94
+ #
95
+ # Dynamoid::TransactionWrite.execute do |t|
96
+ # t.save! user
97
+ # t.create! Account, name: 'A'
98
+ #
99
+ # if user.is_admin?
100
+ # raise Dynamoid::Errors::Rollback
101
+ # end
102
+ # end
103
+ #
104
+ # When a transaction is successfully committed or rolled backed -
105
+ # corresponding +#after_commit+ or +#after_rollback+ callbacks are run for
106
+ # each involved model.
107
+ class TransactionWrite
108
+ def self.execute
109
+ transaction = new
110
+
111
+ begin
112
+ yield transaction
113
+ rescue StandardError => e
114
+ transaction.rollback
115
+
116
+ unless e.is_a?(Dynamoid::Errors::Rollback)
117
+ raise e
118
+ end
119
+ else
120
+ transaction.commit
121
+ end
122
+ end
123
+
124
+ def initialize
125
+ @actions = []
126
+ end
127
+
128
+ # Persist all the changes.
129
+ #
130
+ # transaction = Dynamoid::TransactionWrite.new
131
+ # # ...
132
+ # transaction.commit
133
+ def commit
134
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
135
+ return if actions_to_commit.empty?
136
+
137
+ action_requests = actions_to_commit.map(&:action_request)
138
+ Dynamoid.adapter.transact_write_items(action_requests)
139
+ actions_to_commit.each(&:on_commit)
140
+
141
+ nil
142
+ rescue Aws::Errors::ServiceError
143
+ run_on_rollback_callbacks
144
+ raise
145
+ end
146
+
147
+ def rollback
148
+ run_on_rollback_callbacks
149
+ end
150
+
151
+ # Create new model or persist changes in already existing one.
152
+ #
153
+ # Run the validation and callbacks. Returns +true+ if saving is successful
154
+ # and +false+ otherwise.
155
+ #
156
+ # user = User.new
157
+ #
158
+ # Dynamoid::TransactionWrite.execute do |t|
159
+ # t.save!(user)
160
+ # end
161
+ #
162
+ # Validation can be skipped with +validate: false+ option:
163
+ #
164
+ # user = User.new(age: -1)
165
+ #
166
+ # Dynamoid::TransactionWrite.execute do |t|
167
+ # t.save!(user, validate: false)
168
+ # end
169
+ #
170
+ # +save!+ by default sets timestamps attributes - +created_at+ and
171
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
172
+ # when updates already existing one.
173
+ #
174
+ # If a model is new and hash key (+id+ by default) is not assigned yet
175
+ # it was assigned implicitly with random UUID value.
176
+ #
177
+ # When a model is not persisted - its id should have unique value.
178
+ # Otherwise a transaction will be rolled back.
179
+ #
180
+ # @param model [Dynamoid::Document] a model
181
+ # @param options [Hash] (optional)
182
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
183
+ # @return [true|false] Whether saving successful or not
184
+ def save!(model, **options)
185
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: true)
186
+ register_action action
187
+ end
188
+
189
+ # Create new model or persist changes in already existing one.
190
+ #
191
+ # Run the validation and callbacks. Raise
192
+ # +Dynamoid::Errors::DocumentNotValid+ unless this object is valid.
193
+ #
194
+ # user = User.new
195
+ #
196
+ # Dynamoid::TransactionWrite.execute do |t|
197
+ # t.save(user)
198
+ # end
199
+ #
200
+ # Validation can be skipped with +validate: false+ option:
201
+ #
202
+ # user = User.new(age: -1)
203
+ #
204
+ # Dynamoid::TransactionWrite.execute do |t|
205
+ # t.save(user, validate: false)
206
+ # end
207
+ #
208
+ # +save+ by default sets timestamps attributes - +created_at+ and
209
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
210
+ # when updates already existing one.
211
+ #
212
+ # If a model is new and hash key (+id+ by default) is not assigned yet
213
+ # it was assigned implicitly with random UUID value.
214
+ #
215
+ # When a model is not persisted - its id should have unique value.
216
+ # Otherwise a transaction will be rolled back.
217
+ #
218
+ # @param model [Dynamoid::Document] a model
219
+ # @param options [Hash] (optional)
220
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
221
+ # @return [true|false] Whether saving successful or not
222
+ def save(model, **options)
223
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: false)
224
+ register_action action
225
+ end
226
+
227
+ # Create a model.
228
+ #
229
+ # Dynamoid::TransactionWrite.execute do |t|
230
+ # t.create!(User, name: 'A')
231
+ # end
232
+ #
233
+ # Accepts both Hash and Array of Hashes and can create several models.
234
+ #
235
+ # Dynamoid::TransactionWrite.execute do |t|
236
+ # t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
237
+ # end
238
+ #
239
+ # Instantiates a model and pass it into an optional block to set other
240
+ # attributes.
241
+ #
242
+ # Dynamoid::TransactionWrite.execute do |t|
243
+ # t.create!(User, name: 'A') do |user|
244
+ # user.initialize_roles
245
+ # end
246
+ # end
247
+ #
248
+ # Validates model and runs callbacks.
249
+ #
250
+ # @param model_class [Class] a model class which should be instantiated
251
+ # @param attributes [Hash|Array<Hash>] attributes of a model
252
+ # @param block [Proc] a block to process a model after initialization
253
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
254
+ def create!(model_class, attributes = {}, &block)
255
+ if attributes.is_a? Array
256
+ attributes.map do |attr|
257
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block)
258
+ register_action action
259
+ end
260
+ else
261
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block)
262
+ register_action action
263
+ end
264
+ end
265
+
266
+ # Create a model.
267
+ #
268
+ # Dynamoid::TransactionWrite.execute do |t|
269
+ # t.create(User, name: 'A')
270
+ # end
271
+ #
272
+ # Accepts both Hash and Array of Hashes and can create several models.
273
+ #
274
+ # Dynamoid::TransactionWrite.execute do |t|
275
+ # t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
276
+ # end
277
+ #
278
+ # Instantiates a model and pass it into an optional block to set other
279
+ # attributes.
280
+ #
281
+ # Dynamoid::TransactionWrite.execute do |t|
282
+ # t.create(User, name: 'A') do |user|
283
+ # user.initialize_roles
284
+ # end
285
+ # end
286
+ #
287
+ # Validates model and runs callbacks.
288
+ #
289
+ # @param model_class [Class] a model class which should be instantiated
290
+ # @param attributes [Hash|Array<Hash>] attributes of a model
291
+ # @param block [Proc] a block to process a model after initialization
292
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
293
+ def create(model_class, attributes = {}, &block)
294
+ if attributes.is_a? Array
295
+ attributes.map do |attr|
296
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block)
297
+ register_action action
298
+ end
299
+ else
300
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block)
301
+ register_action action
302
+ end
303
+ end
304
+
305
+ # Update an existing document or create a new one.
306
+ #
307
+ # If a document with specified hash and range keys doesn't exist it
308
+ # creates a new document with specified attributes. Doesn't run
309
+ # validations and callbacks.
310
+ #
311
+ # Dynamoid::TransactionWrite.execute do |t|
312
+ # t.upsert(User, '1', age: 26)
313
+ # end
314
+ #
315
+ # If range key is declared for a model it should be passed as well:
316
+ #
317
+ # Dynamoid::TransactionWrite.execute do |t|
318
+ # t.upsert(User, '1', 'Tylor', age: 26)
319
+ # end
320
+ #
321
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
322
+ # attributes is not declared in the model class.
323
+ #
324
+ # @param model_class [Class] a model class
325
+ # @param hash_key [Scalar value] hash key value
326
+ # @param range_key [Scalar value] range key value (optional)
327
+ # @param attributes [Hash]
328
+ # @return [nil]
329
+ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
330
+ action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes)
331
+ register_action action
332
+ end
333
+
334
+ # Update document.
335
+ #
336
+ # Doesn't run validations and callbacks.
337
+ #
338
+ # Dynamoid::TransactionWrite.execute do |t|
339
+ # t.update_fields(User, '1', age: 26)
340
+ # end
341
+ #
342
+ # If range key is declared for a model it should be passed as well:
343
+ #
344
+ # Dynamoid::TransactionWrite.execute do |t|
345
+ # t.update_fields(User, '1', 'Tylor', age: 26)
346
+ # end
347
+ #
348
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
349
+ # attributes is not declared in the model class.
350
+ #
351
+ # @param model_class [Class] a model class
352
+ # @param hash_key [Scalar value] hash key value
353
+ # @param range_key [Scalar value] range key value (optional)
354
+ # @param attributes [Hash]
355
+ # @return [nil]
356
+ def update_fields(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
357
+ action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
358
+ register_action action
359
+ end
360
+
361
+ # Update multiple attributes at once.
362
+ #
363
+ # Dynamoid::TransactionWrite.execute do |t|
364
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
365
+ # end
366
+ #
367
+ # Returns +true+ if saving is successful and +false+
368
+ # otherwise.
369
+ #
370
+ # @param model [Dynamoid::Document] a model
371
+ # @param attributes [Hash] a hash of attributes to update
372
+ # @return [true|false] Whether updating successful or not
373
+ def update_attributes(model, attributes)
374
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false)
375
+ register_action action
376
+ end
377
+
378
+ # Update multiple attributes at once.
379
+ #
380
+ # Returns +true+ if saving is successful and +false+
381
+ # otherwise.
382
+ #
383
+ # Dynamoid::TransactionWrite.execute do |t|
384
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
385
+ # end
386
+ #
387
+ # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
388
+ # fails.
389
+ #
390
+ # @param model [Dynamoid::Document] a model
391
+ # @param attributes [Hash] a hash of attributes to update
392
+ def update_attributes!(model, attributes)
393
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true)
394
+ register_action action
395
+ end
396
+
397
+ # Delete a model.
398
+ #
399
+ # Can be called either with a model:
400
+ #
401
+ # Dynamoid::TransactionWrite.execute do |t|
402
+ # t.delete(user)
403
+ # end
404
+ #
405
+ # or with a primary key:
406
+ #
407
+ # Dynamoid::TransactionWrite.execute do |t|
408
+ # t.delete(User, user_id)
409
+ # end
410
+ #
411
+ # Raise +MissingRangeKey+ if a range key is declared but not passed as argument.
412
+ #
413
+ # @param model_or_model_class [Class|Dynamoid::Document] either model or model class
414
+ # @param hash_key [Scalar value] hash key value
415
+ # @param range_key [Scalar value] range key value (optional)
416
+ # @return [Dynamoid::Document] self
417
+ def delete(model_or_model_class, hash_key = nil, range_key = nil)
418
+ action = if model_or_model_class.is_a? Class
419
+ Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key)
420
+ else
421
+ Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class)
422
+ end
423
+ register_action action
424
+ end
425
+
426
+ # Delete a model.
427
+ #
428
+ # Runs callbacks.
429
+ #
430
+ # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
431
+ # failed (e.g. aborted by a callback).
432
+ #
433
+ # @param model [Dynamoid::Document] a model
434
+ # @return [Dynamoid::Document|false] returns self if destoying is succefull, +false+ otherwise
435
+ def destroy!(model)
436
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
437
+ register_action action
438
+ end
439
+
440
+ # Delete a model.
441
+ #
442
+ # Runs callbacks.
443
+ #
444
+ # @param model [Dynamoid::Document] a model
445
+ # @return [Dynamoid::Document] self
446
+ def destroy(model)
447
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false)
448
+ register_action action
449
+ end
450
+
451
+ private
452
+
453
+ def register_action(action)
454
+ @actions << action
455
+ action.on_registration
456
+ action.observable_by_user_result
457
+ end
458
+
459
+ def run_on_rollback_callbacks
460
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
461
+ actions_to_commit.each(&:on_rollback)
462
+ end
463
+ end
464
+ end
@@ -57,11 +57,12 @@ module Dynamoid
57
57
 
58
58
  class StringTypeCaster < Base
59
59
  def process(value)
60
- if value == true
60
+ case value
61
+ when true
61
62
  't'
62
- elsif value == false
63
+ when false
63
64
  'f'
64
- elsif value.is_a? String
65
+ when String
65
66
  value.dup
66
67
  else
67
68
  value.to_s
@@ -71,6 +72,7 @@ module Dynamoid
71
72
 
72
73
  class IntegerTypeCaster < Base
73
74
  def process(value)
75
+ # rubocop:disable Lint/DuplicateBranch
74
76
  if value == true
75
77
  1
76
78
  elsif value == false
@@ -84,11 +86,13 @@ module Dynamoid
84
86
  else
85
87
  value.to_i
86
88
  end
89
+ # rubocop:enable Lint/DuplicateBranch
87
90
  end
88
91
  end
89
92
 
90
93
  class NumberTypeCaster < Base
91
94
  def process(value)
95
+ # rubocop:disable Lint/DuplicateBranch
92
96
  if value == true
93
97
  1
94
98
  elsif value == false
@@ -104,6 +108,7 @@ module Dynamoid
104
108
  else
105
109
  value.to_d
106
110
  end
111
+ # rubocop:enable Lint/DuplicateBranch
107
112
  end
108
113
  end
109
114
 
@@ -135,7 +140,7 @@ module Dynamoid
135
140
  raise ArgumentError, "Set element type #{element_type} isn't supported"
136
141
  end
137
142
 
138
- set.map { |el| type_caster.process(el) }.to_set
143
+ set.to_set { |el| type_caster.process(el) }
139
144
  end
140
145
 
141
146
  def element_type
@@ -227,10 +232,10 @@ module Dynamoid
227
232
  nil
228
233
  elsif value.is_a?(String)
229
234
  dt = begin
230
- DateTime.parse(value)
231
- rescue StandardError
232
- nil
233
- end
235
+ DateTime.parse(value)
236
+ rescue StandardError
237
+ nil
238
+ end
234
239
  if dt
235
240
  seconds = string_utc_offset(value) || ApplicationTimeZone.utc_offset
236
241
  offset = seconds_to_offset(seconds)
@@ -255,9 +260,7 @@ module Dynamoid
255
260
 
256
261
  class DateTypeCaster < Base
257
262
  def process(value)
258
- if !value.respond_to?(:to_date)
259
- nil
260
- else
263
+ if value.respond_to?(:to_date)
261
264
  begin
262
265
  value.to_date
263
266
  rescue StandardError
@@ -277,17 +280,17 @@ module Dynamoid
277
280
  def process(value)
278
281
  if value == ''
279
282
  nil
280
- elsif [false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF'].include? value
281
- false
282
283
  else
283
- true
284
+ ![false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF'].include? value
284
285
  end
285
286
  end
286
287
  end
287
288
 
288
289
  class BinaryTypeCaster < Base
289
290
  def process(value)
290
- if value.is_a? String
291
+ if value.is_a?(StringIO) || value.is_a?(IO)
292
+ value
293
+ elsif value.is_a?(String)
291
294
  value.dup
292
295
  else
293
296
  value.to_s
@@ -115,7 +115,7 @@ module Dynamoid
115
115
  def process_typed_collection(set)
116
116
  if allowed_type?
117
117
  undumper = Undumping.find_undumper(element_options)
118
- set.map { |el| undumper.process(el) }.to_set
118
+ set.to_set { |el| undumper.process(el) }
119
119
  else
120
120
  raise ArgumentError, "Set element type #{element_type} isn't supported"
121
121
  end
@@ -238,7 +238,8 @@ module Dynamoid
238
238
  class SerializedUndumper < Base
239
239
  # We must use YAML.safe_load in Ruby 3.1 to handle serialized Set class
240
240
  minimum_ruby_version = ->(version) { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version) }
241
- # Once we drop support for Rubies older than 2.6 we can remove this conditional (with major version bump)!
241
+
242
+ # Once we drop support for Rubies older than 2.6 we can remove this condition (with major version bump)!
242
243
  # YAML_SAFE_LOAD = minimum_ruby_version.call("2.6")
243
244
  # But we don't want to change behavior for Ruby <= 3.0 that has been using the gem, without a major version bump
244
245
  YAML_SAFE_LOAD = minimum_ruby_version.call('3.1')
@@ -284,7 +285,17 @@ module Dynamoid
284
285
 
285
286
  class BinaryUndumper < Base
286
287
  def process(value)
287
- Base64.strict_decode64(value)
288
+ store_as_binary = if @options[:store_as_native_binary].nil?
289
+ Dynamoid.config.store_binary_as_native
290
+ else
291
+ @options[:store_as_native_binary]
292
+ end
293
+
294
+ if store_as_binary
295
+ value.string # expect StringIO here
296
+ else
297
+ Base64.strict_decode64(value)
298
+ end
288
299
  end
289
300
  end
290
301
 
@@ -24,7 +24,7 @@ module Dynamoid
24
24
  # @since 0.2.0
25
25
  def valid?(context = nil)
26
26
  context ||= (new_record? ? :create : :update)
27
- super(context)
27
+ super
28
28
  end
29
29
 
30
30
  # Raise an error unless this object is valid.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
- VERSION = '3.9.0'
4
+ VERSION = '3.11.0'
5
5
  end
data/lib/dynamoid.rb CHANGED
@@ -35,6 +35,7 @@ require 'dynamoid/loadable'
35
35
  require 'dynamoid/components'
36
36
  require 'dynamoid/document'
37
37
  require 'dynamoid/adapter'
38
+ require 'dynamoid/transaction_write'
38
39
 
39
40
  require 'dynamoid/tasks/database'
40
41
 
@@ -62,4 +63,10 @@ module Dynamoid
62
63
  def adapter
63
64
  @adapter ||= Adapter.new
64
65
  end
66
+
67
+ # @private
68
+ def deprecator
69
+ # all the deprecated behavior will be removed in the next major version
70
+ @deprecator ||= ActiveSupport::Deprecation.new('4.0', 'Dynamoid')
71
+ end
65
72
  end