dynamoid 3.0.0 → 3.1.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.
@@ -64,6 +64,7 @@ module Dynamoid #:nodoc:
64
64
  end
65
65
  end
66
66
  define_method("#{named}=") { |value| write_attribute(named, value) }
67
+ define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
67
68
  end
68
69
  end
69
70
 
@@ -88,6 +89,7 @@ module Dynamoid #:nodoc:
88
89
  remove_method field
89
90
  remove_method :"#{field}="
90
91
  remove_method :"#{field}?"
92
+ remove_method:"#{field}_before_type_cast"
91
93
  end
92
94
  end
93
95
 
@@ -119,6 +121,8 @@ module Dynamoid #:nodoc:
119
121
  association.reset
120
122
  end
121
123
 
124
+ @attributes_before_type_cast[name] = value
125
+
122
126
  value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
123
127
  attributes[name] = value_casted
124
128
  end
@@ -134,25 +138,17 @@ module Dynamoid #:nodoc:
134
138
  end
135
139
  alias [] read_attribute
136
140
 
137
- # Updates multiple attibutes at once, saving the object once the updates are complete.
138
- #
139
- # @param [Hash] attributes a hash of attributes to update
140
- #
141
- # @since 0.2.0
142
- def update_attributes(attributes)
143
- attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
144
- save
141
+ # Returns a hash of attributes before typecasting
142
+ def attributes_before_type_cast
143
+ @attributes_before_type_cast
145
144
  end
146
145
 
147
- # Update a single attribute, saving the object afterwards.
146
+ # Returns the value of the attribute identified by name before typecasting
148
147
  #
149
- # @param [Symbol] attribute the attribute to update
150
- # @param [Object] value the value to assign it
151
- #
152
- # @since 0.2.0
153
- def update_attribute(attribute, value)
154
- write_attribute(attribute, value)
155
- save
148
+ # @param [Symbol] attribute name
149
+ def read_attribute_before_type_cast(name)
150
+ return nil unless name.respond_to?(:to_sym)
151
+ @attributes_before_type_cast[name.to_sym]
156
152
  end
157
153
 
158
154
  private
@@ -173,8 +169,14 @@ module Dynamoid #:nodoc:
173
169
  end
174
170
  end
175
171
 
176
- def set_type
177
- self.type ||= self.class.name if self.class.attributes[:type]
172
+ def set_inheritance_field
173
+ # actually it does only following logic:
174
+ # self.type ||= self.class.name if self.class.attributes[:type]
175
+
176
+ type = self.class.inheritance_field
177
+ if self.class.attributes[type] && self.send(type).nil?
178
+ self.send("#{type}=", self.class.name)
179
+ end
178
180
  end
179
181
  end
180
182
  end
@@ -37,12 +37,17 @@ module Dynamoid
37
37
  # @example Find several documents by partition key and sort key
38
38
  # Document.find([[101, 'archived'], [102, 'new'], [103, 'deleted']])
39
39
  #
40
+ # @example Perform strong consistent reads
41
+ # Document.find(101, consistent_read: true)
42
+ # Document.find(101, 102, 103, consistent_read: true)
43
+ # Document.find(101, range_key: 'archived', consistent_read: true)
44
+ #
40
45
  # @since 0.2.0
41
46
  def find(*ids, **options)
42
47
  if ids.size == 1 && !ids[0].is_a?(Array)
43
48
  _find_by_id(ids[0], options.merge(raise_error: true))
44
49
  else
45
- _find_all(ids.flatten(1), raise_error: true)
50
+ _find_all(ids.flatten(1), options.merge(raise_error: true))
46
51
  end
47
52
  end
48
53
 
@@ -95,10 +100,12 @@ module Dynamoid
95
100
  end
96
101
  end
97
102
 
103
+ read_options = options.slice(:consistent_read)
104
+
98
105
  items = if Dynamoid.config.backoff
99
106
  items = []
100
107
  backoff = nil
101
- Dynamoid.adapter.read(table_name, ids, options) do |hash, has_unprocessed_items|
108
+ Dynamoid.adapter.read(table_name, ids, read_options) do |hash, has_unprocessed_items|
102
109
  items += hash[table_name]
103
110
 
104
111
  if has_unprocessed_items
@@ -110,14 +117,15 @@ module Dynamoid
110
117
  end
111
118
  items
112
119
  else
113
- items = Dynamoid.adapter.read(table_name, ids, options)
120
+ items = Dynamoid.adapter.read(table_name, ids, read_options)
114
121
  items ? items[table_name] : []
115
122
  end
116
123
 
117
124
  if items.size == ids.size || !options[:raise_error]
118
125
  items ? items.map { |i| from_database(i) } : []
119
126
  else
120
- message = "Couldn't find all #{name.pluralize} with '#{hash_key}': (#{ids.join(', ')}) "
127
+ ids_list = range_key ? ids.map { |pk, sk| "(#{pk},#{sk})" } : ids.map(&:to_s)
128
+ message = "Couldn't find all #{name.pluralize} with primary keys [#{ids_list.join(', ')}] "
121
129
  message += "(found #{items.size} results, but was looking for #{ids.size})"
122
130
  raise Errors::RecordNotFound, message
123
131
  end
@@ -132,10 +140,11 @@ module Dynamoid
132
140
  options[:range_key] = key_dumped
133
141
  end
134
142
 
135
- if item = Dynamoid.adapter.read(table_name, id, options)
143
+ if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
136
144
  from_database(item)
137
145
  elsif options[:raise_error]
138
- message = "Couldn't find #{name} with '#{hash_key}'=#{id}"
146
+ primary_key = range_key ? "(#{id},#{options[:range_key]})" : id
147
+ message = "Couldn't find #{name} with primary key #{primary_key}"
139
148
  raise Errors::RecordNotFound, message
140
149
  end
141
150
  end
@@ -59,7 +59,7 @@ module Dynamoid
59
59
  end
60
60
 
61
61
  def from_database(attrs = {})
62
- clazz = attrs[:type] ? obj = attrs[:type].constantize : self
62
+ clazz = choose_right_class(attrs)
63
63
  attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
64
64
  clazz.new(attrs_undumped).tap { |r| r.new_record = false }
65
65
  end
@@ -151,6 +151,36 @@ module Dynamoid
151
151
  end
152
152
  end
153
153
 
154
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
155
+ #
156
+ # @param [Hash] attributes a hash of attributes to update
157
+ #
158
+ # @since 0.2.0
159
+ def update_attributes(attributes)
160
+ attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
161
+ save
162
+ end
163
+
164
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
165
+ # Raises a Dynamoid::Errors::DocumentNotValid exception if there is vaidation and it fails.
166
+ #
167
+ # @param [Hash] attributes a hash of attributes to update
168
+ def update_attributes!(attributes)
169
+ attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
170
+ save!
171
+ end
172
+
173
+ # Update a single attribute, saving the object afterwards.
174
+ #
175
+ # @param [Symbol] attribute the attribute to update
176
+ # @param [Object] value the value to assign it
177
+ #
178
+ # @since 0.2.0
179
+ def update_attribute(attribute, value)
180
+ write_attribute(attribute, value)
181
+ save
182
+ end
183
+
154
184
  #
155
185
  # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
156
186
  # never cause an update! to fail, but an update! may cause a concurrent save to fail.
@@ -186,6 +216,36 @@ module Dynamoid
186
216
  false
187
217
  end
188
218
 
219
+ # Initializes attribute to zero if nil and adds the value passed as by (default is 1).
220
+ # Only makes sense for number-based attributes. Returns self.
221
+ def increment(attribute, by = 1)
222
+ self[attribute] ||= 0
223
+ self[attribute] += by
224
+ self
225
+ end
226
+
227
+ # Wrapper around increment that saves the record.
228
+ # Returns true if the record could be saved.
229
+ def increment!(attribute, by = 1)
230
+ increment(attribute, by)
231
+ save
232
+ end
233
+
234
+ # Initializes attribute to zero if nil and subtracts the value passed as by (default is 1).
235
+ # Only makes sense for number-based attributes. Returns self.
236
+ def decrement(attribute, by = 1)
237
+ self[attribute] ||= 0
238
+ self[attribute] -= by
239
+ self
240
+ end
241
+
242
+ # Wrapper around decrement that saves the record.
243
+ # Returns true if the record could be saved.
244
+ def decrement!(attribute, by = 1)
245
+ decrement(attribute, by)
246
+ save
247
+ end
248
+
189
249
  # Delete this object, but only after running callbacks for it.
190
250
  #
191
251
  # @since 0.2.0
@@ -7,7 +7,7 @@ namespace :dynamoid do
7
7
  desc 'Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables'
8
8
  task create_tables: :environment do
9
9
  # Load models so Dynamoid will be able to discover tables expected.
10
- Dir[File.join(Dynamoid::Config.models_dir, '*.rb')].sort.each { |file| require file }
10
+ Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
11
11
  if Dynamoid.included_models.any?
12
12
  tables = Dynamoid::Tasks::Database.create_tables
13
13
  result = tables[:created].map { |c| "#{c} created" } + tables[:existing].map { |e| "#{e} already exists" }
@@ -14,7 +14,7 @@ module Dynamoid
14
14
  if Dynamoid.adapter.list_tables.include? model.table_name
15
15
  results[:existing] << model.table_name
16
16
  else
17
- model.create_table
17
+ model.create_table(sync: true)
18
18
  results[:created] << model.table_name
19
19
  end
20
20
  end
@@ -106,6 +106,18 @@ module Dynamoid
106
106
 
107
107
  class SetTypeCaster < Base
108
108
  def process(value)
109
+ set = type_cast_to_set(value)
110
+
111
+ if set.present? && @options[:of].present?
112
+ process_typed_set(set)
113
+ else
114
+ set
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def type_cast_to_set(value)
109
121
  if value.is_a?(Set)
110
122
  value.dup
111
123
  elsif value.respond_to?(:to_set)
@@ -114,10 +126,50 @@ module Dynamoid
114
126
  nil
115
127
  end
116
128
  end
129
+
130
+ def process_typed_set(set)
131
+ type_caster = TypeCasting.find_type_caster(element_options)
132
+
133
+ if type_caster.nil?
134
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
135
+ end
136
+
137
+ set.map { |el| type_caster.process(el) }.to_set
138
+ end
139
+
140
+ def element_type
141
+ unless @options[:of].is_a?(Hash)
142
+ @options[:of]
143
+ else
144
+ @options[:of].keys.first
145
+ end
146
+ end
147
+
148
+ def element_options
149
+ unless @options[:of].is_a?(Hash)
150
+ { type: element_type }
151
+ else
152
+ @options[:of][element_type].dup.tap do |options|
153
+ options[:type] = element_type
154
+ end
155
+ end
156
+ end
117
157
  end
118
158
 
119
159
  class ArrayTypeCaster < Base
120
160
  def process(value)
161
+ array = type_cast_to_array(value)
162
+
163
+ if array.present? && @options[:of].present?
164
+ process_typed_array(array)
165
+ else
166
+ array
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def type_cast_to_array(value)
121
173
  if value.is_a?(Array)
122
174
  value.dup
123
175
  elsif value.respond_to?(:to_a)
@@ -126,6 +178,34 @@ module Dynamoid
126
178
  nil
127
179
  end
128
180
  end
181
+
182
+ def process_typed_array(array)
183
+ type_caster = TypeCasting.find_type_caster(element_options)
184
+
185
+ if type_caster.nil?
186
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
187
+ end
188
+
189
+ array.map { |el| type_caster.process(el) }
190
+ end
191
+
192
+ def element_type
193
+ unless @options[:of].is_a?(Hash)
194
+ @options[:of]
195
+ else
196
+ @options[:of].keys.first
197
+ end
198
+ end
199
+
200
+ def element_options
201
+ unless @options[:of].is_a?(Hash)
202
+ { type: element_type }
203
+ else
204
+ @options[:of][element_type].dup.tap do |options|
205
+ options[:type] = element_type
206
+ end
207
+ end
208
+ end
129
209
  end
130
210
 
131
211
  class DateTimeTypeCaster < Base
@@ -4,20 +4,24 @@ module Dynamoid
4
4
  module Undumping
5
5
  def self.undump_attributes(attributes, attributes_options)
6
6
  {}.tap do |h|
7
- attributes.symbolize_keys.each do |attribute, value|
8
- h[attribute] = undump_field(value, attributes_options[attribute])
7
+ # ignore existing attributes not declared in document class
8
+ attributes.symbolize_keys
9
+ .select { |attribute| attributes_options.key?(attribute) }
10
+ .each do |attribute, value|
11
+ h[attribute] = undump_field(value, attributes_options[attribute])
9
12
  end
10
13
  end
11
14
  end
12
15
 
13
16
  def self.undump_field(value, options)
17
+ return nil if value.nil?
18
+
14
19
  undumper = find_undumper(options)
15
20
 
16
21
  if undumper.nil?
17
22
  raise ArgumentError, "Unknown type #{options[:type]}"
18
23
  end
19
24
 
20
- return nil if value.nil?
21
25
  undumper.process(value)
22
26
  end
23
27
 
@@ -64,19 +68,93 @@ module Dynamoid
64
68
  end
65
69
 
66
70
  class SetUndumper < Base
67
- def process(value)
68
- case @options[:of]
69
- when :integer
70
- value.map { |v| Integer(v) }.to_set
71
- when :number
72
- value.map { |v| BigDecimal(v.to_s) }.to_set
71
+ ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
72
+
73
+ def process(set)
74
+ if @options.key?(:of)
75
+ process_typed_collection(set)
76
+ else
77
+ set.is_a?(Set) ? set : Set.new(set)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def process_typed_collection(set)
84
+ if allowed_type?
85
+ undumper = Undumping.find_undumper(element_options)
86
+ set.map { |el| undumper.process(el) }.to_set
73
87
  else
74
- value.is_a?(Set) ? value : Set.new(value)
88
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
89
+ end
90
+ end
91
+
92
+ def allowed_type?
93
+ ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
94
+ end
95
+
96
+ def element_type
97
+ unless @options[:of].is_a?(Hash)
98
+ @options[:of]
99
+ else
100
+ @options[:of].keys.first
101
+ end
102
+ end
103
+
104
+ def element_options
105
+ unless @options[:of].is_a?(Hash)
106
+ { type: element_type }
107
+ else
108
+ @options[:of][element_type].dup.tap do |options|
109
+ options[:type] = element_type
110
+ end
75
111
  end
76
112
  end
77
113
  end
78
114
 
79
115
  class ArrayUndumper < Base
116
+ ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
117
+
118
+ def process(array)
119
+ if @options.key?(:of)
120
+ process_typed_collection(array)
121
+ else
122
+ array.is_a?(Array) ? array : Array(array)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def process_typed_collection(array)
129
+ if allowed_type?
130
+ undumper = Undumping.find_undumper(element_options)
131
+ array.map { |el| undumper.process(el) }
132
+ else
133
+ raise ArgumentError, "Array element type #{element_type} isn't supported"
134
+ end
135
+ end
136
+
137
+ def allowed_type?
138
+ ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
139
+ end
140
+
141
+ def element_type
142
+ unless @options[:of].is_a?(Hash)
143
+ @options[:of]
144
+ else
145
+ @options[:of].keys.first
146
+ end
147
+ end
148
+
149
+ def element_options
150
+ unless @options[:of].is_a?(Hash)
151
+ { type: element_type }
152
+ else
153
+ @options[:of][element_type].dup.tap do |options|
154
+ options[:type] = element_type
155
+ end
156
+ end
157
+ end
80
158
  end
81
159
 
82
160
  class DateTimeUndumper < Base