dynamoid 1.3.4 → 2.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +37 -7
  5. data/Appraisals +11 -0
  6. data/CHANGELOG.md +115 -2
  7. data/Gemfile +2 -0
  8. data/LICENSE.txt +18 -16
  9. data/README.md +253 -34
  10. data/Rakefile +0 -24
  11. data/Vagrantfile +1 -1
  12. data/docker-compose.yml +7 -0
  13. data/dynamoid.gemspec +4 -4
  14. data/gemfiles/rails_4_0.gemfile +3 -3
  15. data/gemfiles/rails_4_1.gemfile +3 -3
  16. data/gemfiles/rails_4_2.gemfile +3 -3
  17. data/gemfiles/rails_5_0.gemfile +2 -1
  18. data/gemfiles/rails_5_1.gemfile +8 -0
  19. data/gemfiles/rails_5_2.gemfile +8 -0
  20. data/lib/dynamoid.rb +31 -31
  21. data/lib/dynamoid/adapter.rb +14 -10
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +188 -100
  23. data/lib/dynamoid/associations.rb +21 -12
  24. data/lib/dynamoid/associations/association.rb +19 -3
  25. data/lib/dynamoid/associations/belongs_to.rb +26 -16
  26. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -16
  27. data/lib/dynamoid/associations/has_many.rb +2 -17
  28. data/lib/dynamoid/associations/has_one.rb +0 -14
  29. data/lib/dynamoid/associations/many_association.rb +19 -6
  30. data/lib/dynamoid/associations/single_association.rb +25 -7
  31. data/lib/dynamoid/config.rb +37 -18
  32. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +11 -0
  33. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +25 -0
  34. data/lib/dynamoid/config/options.rb +1 -1
  35. data/lib/dynamoid/criteria/chain.rb +48 -32
  36. data/lib/dynamoid/dirty.rb +23 -4
  37. data/lib/dynamoid/document.rb +88 -5
  38. data/lib/dynamoid/errors.rb +4 -1
  39. data/lib/dynamoid/fields.rb +6 -6
  40. data/lib/dynamoid/finders.rb +42 -12
  41. data/lib/dynamoid/identity_map.rb +0 -1
  42. data/lib/dynamoid/indexes.rb +41 -54
  43. data/lib/dynamoid/persistence.rb +151 -40
  44. data/lib/dynamoid/railtie.rb +1 -1
  45. data/lib/dynamoid/validations.rb +4 -3
  46. data/lib/dynamoid/version.rb +1 -1
  47. metadata +18 -29
  48. data/gemfiles/rails_4_0.gemfile.lock +0 -150
  49. data/gemfiles/rails_4_1.gemfile.lock +0 -154
  50. data/gemfiles/rails_4_2.gemfile.lock +0 -175
  51. data/gemfiles/rails_5_0.gemfile.lock +0 -180
@@ -23,7 +23,7 @@ module Dynamoid #:nodoc:
23
23
  field :created_at, :datetime
24
24
  field :updated_at, :datetime
25
25
 
26
- field :id #Default primary key
26
+ field :id # Default primary key
27
27
  end
28
28
 
29
29
  module ClassMethods
@@ -50,7 +50,7 @@ module Dynamoid #:nodoc:
50
50
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
51
51
  type = :number
52
52
  end
53
- self.attributes = attributes.merge(name => {:type => type}.merge(options))
53
+ self.attributes = attributes.merge(name => {type: type}.merge(options))
54
54
 
55
55
  generated_methods.module_eval do
56
56
  define_method(named) { read_attribute(named) }
@@ -67,13 +67,13 @@ module Dynamoid #:nodoc:
67
67
  end
68
68
  end
69
69
 
70
- def range(name, type = :string)
71
- field(name, type)
70
+ def range(name, type = :string, options = {})
71
+ field(name, type, options)
72
72
  self.range_key = name
73
73
  end
74
74
 
75
75
  def table(options)
76
- #a default 'id' column is created when Dynamoid::Document is included
76
+ # a default 'id' column is created when Dynamoid::Document is included
77
77
  unless(attributes.has_key? hash_key)
78
78
  remove_field :id
79
79
  field(hash_key)
@@ -82,7 +82,7 @@ module Dynamoid #:nodoc:
82
82
 
83
83
  def remove_field(field)
84
84
  field = field.to_sym
85
- attributes.delete(field) or raise "No such field"
85
+ attributes.delete(field) or raise 'No such field'
86
86
 
87
87
  generated_methods.module_eval do
88
88
  remove_method field
@@ -36,15 +36,27 @@ module Dynamoid
36
36
  ids = Array(ids.flatten.uniq)
37
37
  if ids.count == 1
38
38
  result = self.find_by_id(ids.first, options)
39
+ if result.nil?
40
+ message = "Couldn't find #{self.name} with '#{self.hash_key}'=#{ids[0]}"
41
+ raise Errors::RecordNotFound.new(message)
42
+ end
39
43
  expects_array ? Array(result) : result
40
44
  else
41
- find_all(ids)
45
+ result = find_all(ids)
46
+ if result.size != ids.size
47
+ message = "Couldn't find all #{self.name.pluralize} with '#{self.hash_key}': (#{ids.join(', ')}) "
48
+ message << "(found #{result.size} results, but was looking for #{ids.size})"
49
+ raise Errors::RecordNotFound.new(message)
50
+ end
51
+ result
42
52
  end
43
53
  end
44
54
 
45
- # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGet.
55
+ # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGetItem.
46
56
  # Returns empty array if no results found.
47
57
  #
58
+ # Uses backoff specified by `Dynamoid::Config.backoff` config option
59
+ #
48
60
  # @param [Array<ID>] ids
49
61
  # @param [Hash] options: Passed to the underlying query.
50
62
  #
@@ -55,8 +67,26 @@ module Dynamoid
55
67
  # find all the tweets using hash key and range key with consistent read
56
68
  # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
57
69
  def find_all(ids, options = {})
58
- items = Dynamoid.adapter.read(self.table_name, ids, options)
59
- items ? items[self.table_name].map{|i| from_database(i)} : []
70
+ results = unless Dynamoid.config.backoff
71
+ items = Dynamoid.adapter.read(self.table_name, ids, options)
72
+ items ? items[self.table_name] : []
73
+ else
74
+ items = []
75
+ backoff = nil
76
+ Dynamoid.adapter.read(self.table_name, ids, options) do |hash, has_unprocessed_items|
77
+ items += hash[self.table_name]
78
+
79
+ if has_unprocessed_items
80
+ backoff ||= Dynamoid.config.build_backoff
81
+ backoff.call
82
+ else
83
+ backoff = nil
84
+ end
85
+ end
86
+ items
87
+ end
88
+
89
+ results ? results.map {|i| from_database(i) } : []
60
90
  end
61
91
 
62
92
  # Find one object directly by id.
@@ -80,7 +110,7 @@ module Dynamoid
80
110
  # @param [String/Number] range_key of the object to find
81
111
  #
82
112
  def find_by_composite_key(hash_key, range_key, options = {})
83
- find_by_id(hash_key, options.merge({:range_key => range_key}))
113
+ find_by_id(hash_key, options.merge(range_key: range_key))
84
114
  end
85
115
 
86
116
  # Find all objects by hash and range keys.
@@ -105,7 +135,7 @@ module Dynamoid
105
135
  # @return [Array] an array of all matching items
106
136
  #
107
137
  def find_all_by_composite_key(hash_key, options = {})
108
- Dynamoid.adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
138
+ Dynamoid.adapter.query(self.table_name, options.merge(hash_value: hash_key)).collect do |item|
109
139
  from_database(item)
110
140
  end
111
141
  end
@@ -138,9 +168,9 @@ module Dynamoid
138
168
 
139
169
  if range_key_field
140
170
  range_key_field = range_key_field.to_s
141
- range_key_op = "eq"
142
- if range_key_field.include?(".")
143
- range_key_field, range_key_op = range_key_field.split(".", 2)
171
+ range_key_op = 'eq'
172
+ if range_key_field.include?('.')
173
+ range_key_field, range_key_op = range_key_field.split('.', 2)
144
174
  end
145
175
  range_op_mapped = RANGE_MAP.fetch(range_key_op)
146
176
  end
@@ -151,9 +181,9 @@ module Dynamoid
151
181
 
152
182
  # query
153
183
  opts = {
154
- :hash_key => hash_key_field.to_s,
155
- :hash_value => hash_key_value,
156
- :index_name => index.name,
184
+ hash_key: hash_key_field.to_s,
185
+ hash_value: hash_key_value,
186
+ index_name: index.name,
157
187
  }
158
188
  if range_key_field
159
189
  opts[:range_key] = range_key_field
@@ -80,7 +80,6 @@ module Dynamoid
80
80
  super
81
81
  end
82
82
 
83
-
84
83
  def identity_map_key
85
84
  key = hash_key.to_s
86
85
  if self.class.range_key
@@ -39,8 +39,8 @@ module Dynamoid
39
39
  end
40
40
 
41
41
  index_opts = {
42
- :read_capacity => Dynamoid::Config.read_capacity,
43
- :write_capacity => Dynamoid::Config.write_capacity
42
+ read_capacity: Dynamoid::Config.read_capacity,
43
+ write_capacity: Dynamoid::Config.write_capacity
44
44
  }.merge(options)
45
45
 
46
46
  index_opts[:dynamoid_class] = self
@@ -52,7 +52,6 @@ module Dynamoid
52
52
  self
53
53
  end
54
54
 
55
-
56
55
  # Defines a local secondary index on a table. Will use the same primary
57
56
  # hash key as the table.
58
57
  #
@@ -83,11 +82,10 @@ module Dynamoid
83
82
  ' must use a different :range_key than the primary key')
84
83
  end
85
84
 
86
- index_opts = options.merge({
87
- :dynamoid_class => self,
88
- :type => :local_secondary,
89
- :hash_key => primary_hash_key
90
- })
85
+ index_opts = options.merge(
86
+ dynamoid_class: self,
87
+ type: :local_secondary,
88
+ hash_key: primary_hash_key)
91
89
 
92
90
  index = Dynamoid::Indexes::Index.new(index_opts)
93
91
  key = index_key(primary_hash_key, index_range_key)
@@ -95,13 +93,11 @@ module Dynamoid
95
93
  self
96
94
  end
97
95
 
98
-
99
96
  def find_index(hash, range=nil)
100
97
  index = self.indexes[index_key(hash, range)]
101
98
  index
102
99
  end
103
100
 
104
-
105
101
  # Returns true iff the provided hash[,range] key combo is a local
106
102
  # secondary index.
107
103
  #
@@ -113,7 +109,6 @@ module Dynamoid
113
109
  self.local_secondary_indexes[index_key(hash, range)].present?
114
110
  end
115
111
 
116
-
117
112
  # Returns true iff the provided hash[,range] key combo is a global
118
113
  # secondary index.
119
114
  #
@@ -125,7 +120,6 @@ module Dynamoid
125
120
  self.global_secondary_indexes[index_key(hash, range)].present?
126
121
  end
127
122
 
128
-
129
123
  # Generates a convenient lookup key name for a hash/range index.
130
124
  # Should normally not be used directly.
131
125
  #
@@ -140,7 +134,6 @@ module Dynamoid
140
134
  name
141
135
  end
142
136
 
143
-
144
137
  # Generates a default index name.
145
138
  #
146
139
  # @param [Symbol] hash hash key name.
@@ -150,7 +143,6 @@ module Dynamoid
150
143
  "#{self.table_name}_index_#{self.index_key(hash, range)}"
151
144
  end
152
145
 
153
-
154
146
  # Convenience method to return all indexes on the table.
155
147
  #
156
148
  # @return [Hash<String, Object>] the combined hash of global and local
@@ -166,7 +158,6 @@ module Dynamoid
166
158
  end
167
159
  end
168
160
 
169
-
170
161
  # Represents the attributes of a DynamoDB index.
171
162
  class Index
172
163
  include ActiveModel::Validations
@@ -178,7 +169,6 @@ module Dynamoid
178
169
  :hash_key_schema, :range_key_schema, :projected_attributes,
179
170
  :read_capacity, :write_capacity
180
171
 
181
-
182
172
  validate do
183
173
  validate_index_type
184
174
  validate_hash_key
@@ -186,7 +176,6 @@ module Dynamoid
186
176
  validate_projected_attributes
187
177
  end
188
178
 
189
-
190
179
  def initialize(attrs={})
191
180
  unless attrs[:dynamoid_class].present?
192
181
  raise Dynamoid::Errors::InvalidIndex.new(':dynamoid_class is required')
@@ -205,7 +194,6 @@ module Dynamoid
205
194
  raise Dynamoid::Errors::InvalidIndex.new(self) unless self.valid?
206
195
  end
207
196
 
208
-
209
197
  # Convenience method to determine the projection type for an index.
210
198
  # Projection types are: :keys_only, :all, :include.
211
199
  #
@@ -218,56 +206,55 @@ module Dynamoid
218
206
  end
219
207
  end
220
208
 
221
-
222
209
  private
223
210
 
224
- def validate_projected_attributes
225
- unless (@projected_attributes.is_a?(Array) ||
226
- PROJECTION_TYPES.include?(@projected_attributes))
227
- errors.add(:projected_attributes, 'Invalid projected attributes specified.')
228
- end
211
+ def validate_projected_attributes
212
+ unless (@projected_attributes.is_a?(Array) ||
213
+ PROJECTION_TYPES.include?(@projected_attributes))
214
+ errors.add(:projected_attributes, 'Invalid projected attributes specified.')
229
215
  end
216
+ end
230
217
 
231
- def validate_index_type
232
- unless (@type.present? &&
233
- [:local_secondary, :global_secondary].include?(@type))
234
- errors.add(:type, 'Invalid index :type specified')
235
- end
218
+ def validate_index_type
219
+ unless (@type.present? &&
220
+ [:local_secondary, :global_secondary].include?(@type))
221
+ errors.add(:type, 'Invalid index :type specified')
236
222
  end
223
+ end
237
224
 
238
- def validate_range_key
239
- if @range_key.present?
240
- range_field_attributes = @dynamoid_class.attributes[@range_key]
241
- if range_field_attributes.present?
242
- range_key_type = range_field_attributes[:type]
243
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
244
- @range_key_schema = {
245
- @range_key => @dynamoid_class.dynamo_type(range_key_type)
246
- }
247
- else
248
- errors.add(:range_key, 'Index :range_key is not a valid key type')
249
- end
225
+ def validate_range_key
226
+ if @range_key.present?
227
+ range_field_attributes = @dynamoid_class.attributes[@range_key]
228
+ if range_field_attributes.present?
229
+ range_key_type = range_field_attributes[:type]
230
+ if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
231
+ @range_key_schema = {
232
+ @range_key => @dynamoid_class.dynamo_type(range_key_type)
233
+ }
250
234
  else
251
- errors.add(:range_key, "No such field #{@range_key} defined on table")
235
+ errors.add(:range_key, 'Index :range_key is not a valid key type')
252
236
  end
237
+ else
238
+ errors.add(:range_key, "No such field #{@range_key} defined on table")
253
239
  end
254
240
  end
241
+ end
255
242
 
256
- def validate_hash_key
257
- hash_field_attributes = @dynamoid_class.attributes[@hash_key]
258
- if hash_field_attributes.present?
259
- hash_field_type = hash_field_attributes[:type]
260
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
261
- @hash_key_schema = {
262
- @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
263
- }
264
- else
265
- errors.add(:hash_key, 'Index :hash_key is not a valid key type')
266
- end
243
+ def validate_hash_key
244
+ hash_field_attributes = @dynamoid_class.attributes[@hash_key]
245
+ if hash_field_attributes.present?
246
+ hash_field_type = hash_field_attributes[:type]
247
+ if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
248
+ @hash_key_schema = {
249
+ @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
250
+ }
267
251
  else
268
- errors.add(:hash_key, "No such field #{@hash_key} defined on table")
252
+ errors.add(:hash_key, 'Index :hash_key is not a valid key type')
269
253
  end
254
+ else
255
+ errors.add(:hash_key, "No such field #{@hash_key} defined on table")
270
256
  end
257
+ end
271
258
  end
272
259
  end
273
260
  end
@@ -21,7 +21,7 @@ module Dynamoid
21
21
  table_base_name = options[:name] || base_class.name.split('::').last
22
22
  .downcase.pluralize
23
23
 
24
- @table_name ||= [Dynamoid::Config.namespace.to_s,table_base_name].reject(&:empty?).join("_")
24
+ @table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
25
25
  end
26
26
 
27
27
  # Creates a table.
@@ -41,14 +41,14 @@ module Dynamoid
41
41
  range_key_hash = nil
42
42
  end
43
43
  options = {
44
- :id => self.hash_key,
45
- :table_name => self.table_name,
46
- :write_capacity => self.write_capacity,
47
- :read_capacity => self.read_capacity,
48
- :range_key => range_key_hash,
49
- :hash_key_type => dynamo_type(attributes[self.hash_key][:type]),
50
- :local_secondary_indexes => self.local_secondary_indexes.values,
51
- :global_secondary_indexes => self.global_secondary_indexes.values
44
+ id: self.hash_key,
45
+ table_name: self.table_name,
46
+ write_capacity: self.write_capacity,
47
+ read_capacity: self.read_capacity,
48
+ range_key: range_key_hash,
49
+ hash_key_type: dynamo_type(attributes[self.hash_key][:type]),
50
+ local_secondary_indexes: self.local_secondary_indexes.values,
51
+ global_secondary_indexes: self.global_secondary_indexes.values
52
52
  }.merge(options)
53
53
 
54
54
  Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
@@ -74,9 +74,7 @@ module Dynamoid
74
74
  if incoming.has_key?(attribute)
75
75
  hash[attribute] = undump_field(incoming[attribute], options)
76
76
  elsif options.has_key?(:default)
77
- default_value = options[:default]
78
- value = default_value.respond_to?(:call) ? default_value.call : default_value.dup
79
- hash[attribute] = value
77
+ hash[attribute] = evaluate_default_value(options[:default])
80
78
  else
81
79
  hash[attribute] = nil
82
80
  end
@@ -119,34 +117,22 @@ module Dynamoid
119
117
  value
120
118
  end
121
119
  when :set
122
- Set.new(value)
120
+ undump_set(options, value)
123
121
  when :datetime
124
- if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
125
- value
126
- else
127
- case Dynamoid::Config.application_timezone
128
- when :utc
129
- ActiveSupport::TimeZone['UTC'].at(value).to_datetime
130
- when :local
131
- Time.at(value).to_datetime
132
- when String
133
- ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
134
- end
135
- end
122
+ parse_datetime(value, options)
136
123
  when :date
137
124
  if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
138
125
  value.to_date
139
126
  else
140
- UNIX_EPOCH_DATE + value.to_i
127
+ parse_date(value, options)
141
128
  end
142
129
  when :boolean
143
- # persisted as 't', but because undump is called during initialize it can come in as true
144
130
  if value == 't' || value == true
145
131
  true
146
132
  elsif value == 'f' || value == false
147
133
  false
148
134
  else
149
- raise ArgumentError, "Boolean column neither true nor false"
135
+ raise ArgumentError, 'Boolean column neither true nor false'
150
136
  end
151
137
  else
152
138
  raise ArgumentError, "Unknown type #{options[:type]}"
@@ -155,6 +141,17 @@ module Dynamoid
155
141
  end
156
142
  end
157
143
 
144
+ def undump_set(options, value)
145
+ case options[:of]
146
+ when :integer
147
+ value.map { |v| Integer(v) }.to_set
148
+ when :number
149
+ value.map { |v| BigDecimal.new(v.to_s) }.to_set
150
+ else
151
+ value.is_a?(Set) ? value : Set.new(value)
152
+ end
153
+ end
154
+
158
155
  def dump_field(value, options)
159
156
  if (field_class = options[:type]).is_a?(Class)
160
157
  if value.respond_to?(:dynamoid_dump)
@@ -177,15 +174,23 @@ module Dynamoid
177
174
  when :array
178
175
  !value.nil? ? value : nil
179
176
  when :datetime
180
- !value.nil? ? value.to_time.to_f : nil
177
+ !value.nil? ? format_datetime(value, options) : nil
181
178
  when :date
182
- !value.nil? ? (value.to_date - UNIX_EPOCH_DATE).to_i : nil
179
+ !value.nil? ? format_date(value, options) : nil
183
180
  when :serialized
184
181
  options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
185
182
  when :raw
186
183
  !value.nil? ? value : nil
187
184
  when :boolean
188
- !value.nil? ? value.to_s[0] : nil
185
+ if !value.nil?
186
+ if options[:store_as_native_boolean]
187
+ !!value # native boolean type
188
+ else
189
+ value.to_s[0] # => "f" or "t"
190
+ end
191
+ else
192
+ nil
193
+ end
189
194
  else
190
195
  raise ArgumentError, "Unknown type #{options[:type]}"
191
196
  end
@@ -207,6 +212,43 @@ module Dynamoid
207
212
  end
208
213
  end
209
214
 
215
+ # Creates several models at once.
216
+ # Neither callbacks nor validations run.
217
+ # It works efficiently because of using BatchWriteItem.
218
+ #
219
+ # Returns array of models
220
+ #
221
+ # Uses backoff specified by `Dynamoid::Config.backoff` config option
222
+ #
223
+ # @param [Array<Hash>] items
224
+ #
225
+ # @example
226
+ # User.import([{ name: 'a' }, { name: 'b' }])
227
+ def import(objects)
228
+ documents = objects.map do |attrs|
229
+ self.build(attrs).tap do |item|
230
+ item.hash_key = SecureRandom.uuid if item.hash_key.blank?
231
+ end
232
+ end
233
+
234
+ unless Dynamoid.config.backoff
235
+ Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
236
+ else
237
+ backoff = nil
238
+ Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) do |has_unprocessed_items|
239
+ if has_unprocessed_items
240
+ backoff ||= Dynamoid.config.build_backoff
241
+ backoff.call
242
+ else
243
+ backoff = nil
244
+ end
245
+ end
246
+ end
247
+
248
+ documents.each { |d| d.new_record = false }
249
+ documents
250
+ end
251
+
210
252
  private
211
253
 
212
254
  def undump_hash(hash)
@@ -231,6 +273,77 @@ module Dynamoid
231
273
  val
232
274
  end
233
275
  end
276
+
277
+ def format_datetime(value, options)
278
+ use_string_format = options[:store_as_string].nil? \
279
+ ? Dynamoid.config.store_datetime_as_string \
280
+ : options[:store_as_string]
281
+
282
+ if use_string_format
283
+ value.to_time.iso8601
284
+ else
285
+ unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
286
+ value = value.to_time
287
+ end
288
+ BigDecimal("%d.%09d" % [value.to_i, value.nsec])
289
+ end
290
+ end
291
+
292
+ def format_date(value, options)
293
+ use_string_format = options[:store_as_string].nil? \
294
+ ? Dynamoid.config.store_date_as_string \
295
+ : options[:store_as_string]
296
+
297
+ unless use_string_format
298
+ (value.to_date - UNIX_EPOCH_DATE).to_i
299
+ else
300
+ value.to_date.iso8601
301
+ end
302
+ end
303
+
304
+ def parse_datetime(value, options)
305
+ return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
306
+
307
+ use_string_format = options[:store_as_string].nil? \
308
+ ? Dynamoid.config.store_datetime_as_string \
309
+ : options[:store_as_string]
310
+ value = DateTime.iso8601(value).to_time.to_i if use_string_format
311
+
312
+ case Dynamoid::Config.application_timezone
313
+ when :utc
314
+ ActiveSupport::TimeZone['UTC'].at(value).to_datetime
315
+ when :local
316
+ Time.at(value).to_datetime
317
+ when String
318
+ ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
319
+ end
320
+ end
321
+
322
+ def parse_date(value, options)
323
+ use_string_format = options[:store_as_string].nil? \
324
+ ? Dynamoid.config.store_date_as_string \
325
+ : options[:store_as_string]
326
+
327
+ unless use_string_format
328
+ UNIX_EPOCH_DATE + value.to_i
329
+ else
330
+ Date.iso8601(value)
331
+ end
332
+ end
333
+
334
+ # Evaluates the default value given, this is used by undump
335
+ # when determining the value of the default given for a field options.
336
+ #
337
+ # @param [Object] :value the attribute's default value
338
+ def evaluate_default_value(val)
339
+ if val.respond_to?(:call)
340
+ val.call
341
+ elsif val.duplicable?
342
+ val.dup
343
+ else
344
+ val
345
+ end
346
+ end
234
347
  end
235
348
 
236
349
  # Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
@@ -256,15 +369,13 @@ module Dynamoid
256
369
  self.class.create_table
257
370
 
258
371
  if new_record?
259
- conditions = { :unless_exists => [self.class.hash_key]}
372
+ conditions = { unless_exists: [self.class.hash_key]}
260
373
  conditions[:unless_exists] << range_key if(range_key)
261
374
 
262
375
  run_callbacks(:create) { persist(conditions) }
263
376
  else
264
377
  persist
265
378
  end
266
-
267
- self
268
379
  end
269
380
 
270
381
  #
@@ -274,10 +385,10 @@ module Dynamoid
274
385
  #
275
386
  def update!(conditions = {}, &block)
276
387
  run_callbacks(:update) do
277
- options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
388
+ options = range_key ? {range_key: dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
278
389
 
279
390
  begin
280
- new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions)) do |t|
391
+ new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(conditions: conditions)) do |t|
281
392
  if(self.class.attributes[:lock_version])
282
393
  t.add(lock_version: 1)
283
394
  end
@@ -316,11 +427,11 @@ module Dynamoid
316
427
  #
317
428
  # @since 0.2.0
318
429
  def delete
319
- options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
430
+ options = range_key ? {range_key: dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
320
431
 
321
432
  # Add an optimistic locking check if the lock_version column exists
322
433
  if(self.class.attributes[:lock_version])
323
- conditions = {:if => {}}
434
+ conditions = {if: {}}
324
435
  conditions[:if][:lock_version] =
325
436
  if changes[:lock_version].nil?
326
437
  self.lock_version
@@ -360,7 +471,7 @@ module Dynamoid
360
471
  # @since 0.2.0
361
472
  def persist(conditions = nil)
362
473
  run_callbacks(:save) do
363
- self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank?
474
+ self.hash_key = SecureRandom.uuid if self.hash_key.blank?
364
475
 
365
476
  # Add an exists check to prevent overwriting existing records with new ones
366
477
  if(new_record?)
@@ -372,7 +483,7 @@ module Dynamoid
372
483
  if(self.class.attributes[:lock_version])
373
484
  conditions ||= {}
374
485
  self.lock_version = (lock_version || 0) + 1
375
- #Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
486
+ # Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
376
487
  (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0])
377
488
  end
378
489