Skip to content

Commit 6061abc

Browse files
authored
Support queries yielding heterogeneous results (#108)
1 parent d807c49 commit 6061abc

File tree

6 files changed

+147
-2
lines changed

6 files changed

+147
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - `Aws::Record::BuildableSearch` - Support queries yielding heterogeneous results using `multi_model_filter` (#107)
5+
46
2.4.1 (2020-05-29)
57
------------------
68

features/searching/search.feature

+4
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ Feature: Amazon DynamoDB Querying and Scanning
190190
]
191191
"""
192192

193+
Scenario: Heterogeneous query
194+
When we run a heterogeneous query
195+
Then we should receive an aws-record collection with multiple model classes
196+
193197
@smart_query
194198
Scenario: Build a Smart Scan
195199
When we run the following search:

features/searching/step_definitions.rb

+18
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,21 @@
6363
SearchTestModel = @model
6464
@collection = eval(code)
6565
end
66+
67+
When(/^we run a heterogeneous query$/) do
68+
@model_1 = @model.dup
69+
@model_2 = @model.dup
70+
scan = @model.build_scan.multi_model_filter do |raw_item_attributes|
71+
if raw_item_attributes['id'] == "1"
72+
@model_1
73+
elsif raw_item_attributes['id'] == "2"
74+
@model_2
75+
end
76+
end
77+
@collection = scan.complete!
78+
end
79+
80+
Then(/^we should receive an aws-record collection with multiple model classes/) do
81+
result_classes = @collection.map(&:class)
82+
expect(result_classes).to include(@model_1, @model_2)
83+
end

lib/aws-record/record/buildable_search.rb

+44
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,50 @@ def limit(size)
178178
self
179179
end
180180

181+
# Allows you to define a callback that will determine the model class
182+
# to be used for each item, allowing queries to return an ItemCollection
183+
# with mixed models. The provided block must return the model class based on
184+
# any logic on the raw item attributes or `nil` if no model applies and
185+
# the item should be skipped. Note: The block only has access to raw item
186+
# data so attributes must be accessed using their names as defined in the
187+
# table, not as the symbols defined in the model class(s).
188+
#
189+
# @example Scan with heterogeneous results:
190+
# # Example model classes
191+
# class Model_A
192+
# include Aws::Record
193+
# set_table_name(TABLE_NAME)
194+
#
195+
# string_attr :uuid, hash_key: true
196+
# string_attr :class_name, range_key: true
197+
#
198+
# string_attr :attr_a
199+
# end
200+
#
201+
# class Model_B
202+
# include Aws::Record
203+
# set_table_name(TABLE_NAME)
204+
#
205+
# string_attr :uuid, hash_key: true
206+
# string_attr :class_name, range_key: true
207+
#
208+
# string_attr :attr_b
209+
# end
210+
#
211+
# # use multi_model_filter to create a query on TABLE_NAME
212+
# items = Model_A.build_scan.multi_model_filter do |raw_item_attributes|
213+
# case raw_item_attributes['class_name']
214+
# when "A" then Model_A
215+
# when "B" then Model_B
216+
# else
217+
# nil
218+
# end
219+
# end.complete!
220+
def multi_model_filter(proc = nil, &block)
221+
@params[:model_filter] = proc || block
222+
self
223+
end
224+
181225
# You must call this method at the end of any query or scan you build.
182226
#
183227
# @return [Aws::Record::ItemCollection] The item collection lazy

lib/aws-record/record/item_collection.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ItemCollection
1919
def initialize(search_method, search_params, model, client)
2020
@search_method = search_method
2121
@search_params = search_params
22+
@model_filter = @search_params.delete(:model_filter)
2223
@model = model
2324
@client = client
2425
end
@@ -91,9 +92,11 @@ def empty?
9192
def _build_items_from_response(items, model)
9293
ret = []
9394
items.each do |item|
94-
record = model.new
95+
model_class = @model_filter ? @model_filter.call(item) : model
96+
next unless model_class
97+
record = model_class.new
9598
data = record.instance_variable_get("@data")
96-
model.attributes.attributes.each do |name, attr|
99+
model_class.attributes.attributes.each do |name, attr|
97100
data.set_attribute(name, attr.extract(item))
98101
end
99102
data.clean!

spec/aws-record/record/item_collection_spec.rb

+74
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,80 @@ module Record
214214
expect(actual).to eq(expected)
215215
expect(api_requests.size).to eq(1)
216216
end
217+
218+
context 'model_filter is set' do
219+
220+
let(:model_a) do
221+
Class.new do
222+
include(Aws::Record)
223+
set_table_name("TestTable")
224+
integer_attr(:id, hash_key: true)
225+
string_attr(:class_name)
226+
string_attr(:attr_a)
227+
end
228+
end
229+
230+
let(:model_b) do
231+
Class.new do
232+
include(Aws::Record)
233+
set_table_name("TestTable")
234+
integer_attr(:id, hash_key: true)
235+
string_attr(:class_name)
236+
string_attr(:attr_b)
237+
end
238+
end
239+
240+
let(:resp) do
241+
{
242+
items: [
243+
{ 'id' => 1, 'class_name' => 'A', 'attr_a' => 'a' },
244+
{ 'id' => 2, 'class_name' => 'B', 'attr_b' => 'b' },
245+
{ 'id' => 3 }
246+
],
247+
count: 3
248+
}
249+
end
250+
251+
let(:model_filter) do
252+
Proc.new do |raw_item_attributes|
253+
case raw_item_attributes['class_name']
254+
when "A" then model_a
255+
when "B" then model_b
256+
else
257+
nil
258+
end
259+
end
260+
end
261+
262+
before { stub_client.stub_responses(:scan, resp) }
263+
264+
let(:c) do
265+
ItemCollection.new(
266+
:scan,
267+
{table_name: "TestTable", model_filter: model_filter },
268+
model_a,
269+
stub_client
270+
)
271+
end
272+
273+
it 'uses the model proc to determine the returned model classes' do
274+
expected = [model_a, model_b]
275+
actual = c.map { |item| item.class }
276+
expect(actual).to eq(expected)
277+
end
278+
279+
it 'maps class specific attributes' do
280+
actual = c.page
281+
expect(actual[0].attr_a).to eq('a')
282+
expect(actual[1].attr_b).to eq('b')
283+
end
284+
285+
it 'skips items when model_filter returns nil' do
286+
actual = c.page
287+
expect(actual.size).to eq(2)
288+
end
289+
end
290+
217291
end
218292

219293
describe "#empty?" do

0 commit comments

Comments
 (0)