brainstem 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 115ca6ee598304821711410e09788b194b4463d3
4
- data.tar.gz: eb49966eafe33595f32048f087ee7148a0b4da36
3
+ metadata.gz: 9bf2b0cdf9ecd18e7a90f8484238fe6e7f63ad43
4
+ data.tar.gz: 7be99f891a90e26715f553fcf0971931bf51c404
5
5
  SHA512:
6
- metadata.gz: 45af28ac0b45a91167174b0809e79c22a20864cb2facb3c59c23fdad0d71e62dec0299db8f74748a0c0306169aa05eaf756f5564459f43d19edb2b0045bbb30c
7
- data.tar.gz: 8050a810b6b1046d00d7aa961bebe48698604be562f28c73943c5160d4668b47c92ff9c4bb52a03e67eb67804d4f9d5a8391507cd5328b8138546e7febefcdc8
6
+ metadata.gz: 138e7db36bf3692bde7fac6c61777ea8695195d32769dec30555bc2d4b5124679dd8f22e08624503ba3148e64c058bcad31f72b24842f2bf864bb4291ca8e052
7
+ data.tar.gz: 37760c56b41a4fbf8dbb5e19b543229c878e61fd88a7a8400f961140565b70c9b738b1882b7aeee1dc3c41e651695e02ab43ed95efd1483a80eafbdf38abf50c
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ + **1.1.0 - _12/18/2017_
4
+ - Add `meta` key to API responses which includes `page_number`, `page_count`, and `page_size` keys.
5
+
3
6
  + **1.0.0 - _07/20/2017_
4
7
  - Add the capability to generate the documentation extracted from your properly annotated
5
8
  presenters and controllers using `bundle exec brainstem generate [ARGS]`.
@@ -1,60 +1,64 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainstem (1.0.0.pre)
4
+ brainstem (1.1.0)
5
5
  activerecord (>= 4.1)
6
6
  activesupport (>= 4.1)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activemodel (5.0.2)
12
- activesupport (= 5.0.2)
13
- activerecord (5.0.2)
14
- activemodel (= 5.0.2)
15
- activesupport (= 5.0.2)
16
- arel (~> 7.0)
17
- activesupport (5.0.2)
11
+ activemodel (5.1.4)
12
+ activesupport (= 5.1.4)
13
+ activerecord (5.1.4)
14
+ activemodel (= 5.1.4)
15
+ activesupport (= 5.1.4)
16
+ arel (~> 8.0)
17
+ activesupport (5.1.4)
18
18
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
19
  i18n (~> 0.7)
20
20
  minitest (~> 5.1)
21
21
  tzinfo (~> 1.1)
22
- arel (7.1.4)
23
- coderay (1.1.1)
22
+ arel (8.0.0)
23
+ coderay (1.1.2)
24
24
  concurrent-ruby (1.0.5)
25
- database_cleaner (1.5.3)
25
+ database_cleaner (1.6.2)
26
+ db-query-matchers (0.9.0)
27
+ activesupport (>= 4.0, <= 6.0)
28
+ rspec (~> 3.0)
26
29
  diff-lcs (1.3)
27
- i18n (0.8.1)
28
- method_source (0.8.2)
29
- minitest (5.10.1)
30
- pry (0.10.4)
31
- coderay (~> 1.1.0)
32
- method_source (~> 0.8.1)
30
+ i18n (0.9.1)
31
+ concurrent-ruby (~> 1.0)
32
+ method_source (0.9.0)
33
+ minitest (5.10.3)
34
+ pry (0.9.12.6)
35
+ coderay (~> 1.0)
36
+ method_source (~> 0.8)
33
37
  slop (~> 3.4)
34
38
  pry-nav (0.2.4)
35
39
  pry (>= 0.9.10, < 0.11.0)
36
- rake (12.0.0)
40
+ rake (12.2.1)
37
41
  redcarpet (3.4.0)
38
- rr (1.2.0)
39
- rspec (3.5.0)
40
- rspec-core (~> 3.5.0)
41
- rspec-expectations (~> 3.5.0)
42
- rspec-mocks (~> 3.5.0)
43
- rspec-core (3.5.4)
44
- rspec-support (~> 3.5.0)
45
- rspec-expectations (3.5.0)
42
+ rr (1.2.1)
43
+ rspec (3.7.0)
44
+ rspec-core (~> 3.7.0)
45
+ rspec-expectations (~> 3.7.0)
46
+ rspec-mocks (~> 3.7.0)
47
+ rspec-core (3.7.0)
48
+ rspec-support (~> 3.7.0)
49
+ rspec-expectations (3.7.0)
46
50
  diff-lcs (>= 1.2.0, < 2.0)
47
- rspec-support (~> 3.5.0)
48
- rspec-mocks (3.5.0)
51
+ rspec-support (~> 3.7.0)
52
+ rspec-mocks (3.7.0)
49
53
  diff-lcs (>= 1.2.0, < 2.0)
50
- rspec-support (~> 3.5.0)
51
- rspec-support (3.5.0)
54
+ rspec-support (~> 3.7.0)
55
+ rspec-support (3.7.0)
52
56
  slop (3.6.0)
53
57
  sqlite3 (1.3.13)
54
58
  thread_safe (0.3.6)
55
- tzinfo (1.2.2)
59
+ tzinfo (1.2.4)
56
60
  thread_safe (~> 0.1)
57
- yard (0.9.8)
61
+ yard (0.9.9)
58
62
 
59
63
  PLATFORMS
60
64
  ruby
@@ -62,6 +66,7 @@ PLATFORMS
62
66
  DEPENDENCIES
63
67
  brainstem!
64
68
  database_cleaner
69
+ db-query-matchers
65
70
  pry
66
71
  pry-nav
67
72
  rake
data/README.md CHANGED
@@ -216,6 +216,21 @@ Responses will look like the following:
216
216
  # Total number of results that matched the query.
217
217
  count: 5,
218
218
 
219
+ # Information about the request and response.
220
+ meta: {
221
+ # Total number of results that matched the query.
222
+ count: 5,
223
+
224
+ # Current page returned in the response.
225
+ page_number: 1,
226
+
227
+ # Total number pages available.
228
+ page_count: 1,
229
+
230
+ # Number of results per page.
231
+ page_size: 20,
232
+ },
233
+
219
234
  # A lookup table to top-level keys. Necessary
220
235
  # because some objects can have associations of
221
236
  # the same type as themselves. Also helps to
@@ -31,4 +31,5 @@ Gem::Specification.new do |gem|
31
31
  gem.add_development_dependency "yard"
32
32
  gem.add_development_dependency "pry"
33
33
  gem.add_development_dependency "pry-nav"
34
+ gem.add_development_dependency "db-query-matchers"
34
35
  end
@@ -53,17 +53,18 @@ module Brainstem
53
53
  options[:default_max_per_page] = default_max_per_page
54
54
  options[:default_max_filter_and_search_page] = default_max_filter_and_search_page
55
55
 
56
- primary_models, count = strategy(options, scope).execute(scope)
56
+ strategy = get_strategy(options, scope)
57
+ primary_models, count = strategy.execute(scope)
57
58
 
58
59
  # Determine if an exception should be raised on an empty result set.
59
60
  if options[:raise_on_empty] && primary_models.empty?
60
61
  raise options[:empty_error_class] || ActiveRecord::RecordNotFound
61
62
  end
62
63
 
63
- structure_response(presented_class, primary_models, count, options)
64
+ structure_response(presented_class, primary_models, strategy, count, options)
64
65
  end
65
66
 
66
- def structure_response(presented_class, primary_models, count, options)
67
+ def structure_response(presented_class, primary_models, strategy, count, options)
67
68
  # key these models will use in the struct that is output
68
69
  brainstem_key = brainstem_key_for!(presented_class)
69
70
 
@@ -71,8 +72,19 @@ module Brainstem
71
72
  selected_associations = filter_includes(options)
72
73
 
73
74
  optional_fields = filter_optional_fields(options)
74
-
75
- struct = { 'count' => count, brainstem_key => {}, 'results' => [] }
75
+ page_size = strategy.calculate_per_page
76
+
77
+ struct = {
78
+ 'count' => count,
79
+ 'results' => [],
80
+ brainstem_key => {},
81
+ 'meta' => {
82
+ 'count' => count,
83
+ 'page_count' => count > 0 ? (count.to_f / page_size).ceil : 0,
84
+ 'page_number' => count > 0 ? options[:params].fetch(:page, 1).to_i : 0,
85
+ 'page_size' => page_size,
86
+ }
87
+ }
76
88
 
77
89
  # Build top-level keys for all requested associations.
78
90
  selected_associations.each do |association|
@@ -145,7 +157,7 @@ module Brainstem
145
157
 
146
158
  private
147
159
 
148
- def strategy(options, scope)
160
+ def get_strategy(options, scope)
149
161
  strat = options[:primary_presenter].get_query_strategy
150
162
 
151
163
  return Brainstem::QueryStrategies::FilterAndSearch.new(options) if strat == :filter_and_search && searching?(options)
@@ -26,6 +26,12 @@ module Brainstem
26
26
  end
27
27
  end
28
28
 
29
+ def calculate_per_page
30
+ per_page = [(@options[:params][:per_page] || @options[:per_page] || @options[:default_per_page]).to_i, (@options[:max_per_page] || @options[:default_max_per_page]).to_i].min
31
+ per_page = @options[:default_per_page] if per_page < 1
32
+ per_page
33
+ end
34
+
29
35
  private
30
36
 
31
37
  def calculate_limit
@@ -36,12 +42,6 @@ module Brainstem
36
42
  [@options[:params][:offset].to_i, 0].max
37
43
  end
38
44
 
39
- def calculate_per_page
40
- per_page = [(@options[:params][:per_page] || @options[:per_page] || @options[:default_per_page]).to_i, (@options[:max_per_page] || @options[:default_max_per_page]).to_i].min
41
- per_page = @options[:default_per_page] if per_page < 1
42
- per_page
43
- end
44
-
45
45
  def calculate_page
46
46
  [(@options[:params][:page] || 1).to_i, 1].max
47
47
  end
@@ -57,6 +57,33 @@ module Brainstem
57
57
  end
58
58
  end
59
59
  end
60
+
61
+ def order_for_search(records, ordered_search_ids, options = {})
62
+ ids_to_position = {}
63
+ ordered_records = []
64
+
65
+ ordered_search_ids.each_with_index do |id, index|
66
+ ids_to_position[id] = index
67
+ end
68
+
69
+ records.each do |record|
70
+ ordered_records[ids_to_position[options[:with_ids] ? record : record.id]] = record
71
+ end
72
+
73
+ ordered_records.compact
74
+ end
75
+
76
+ def calculate_limit_and_offset
77
+ if @options[:params][:limit].present? && @options[:params][:offset].present?
78
+ limit = calculate_limit
79
+ offset = calculate_offset
80
+ else
81
+ limit = calculate_per_page
82
+ offset = limit * (calculate_page - 1)
83
+ end
84
+
85
+ [limit, offset]
86
+ end
60
87
  end
61
88
  end
62
89
  end
@@ -2,13 +2,31 @@ module Brainstem
2
2
  module QueryStrategies
3
3
  class FilterAndSearch < BaseStrategy
4
4
  def execute(scope)
5
- ordered_search_ids = run_search(scope, filter_includes.map(&:name))
6
- scope = scope.where(id: ordered_search_ids)
5
+ scope, ordered_search_ids = run_search(scope, filter_includes.map(&:name))
7
6
  scope = @options[:primary_presenter].apply_filters_to_scope(scope, @options[:params], @options)
8
- scope = @options[:primary_presenter].apply_ordering_to_scope(scope, @options[:params])
9
- count = scope.count
10
- scope = paginate(scope)
11
- primary_models = evaluate_scope(scope)
7
+
8
+ if ordering?
9
+ count = scope.count
10
+ scope = paginate(scope)
11
+ scope = @options[:primary_presenter].apply_ordering_to_scope(scope, @options[:params])
12
+ primary_models = evaluate_scope(scope)
13
+ else
14
+ filtered_ids = scope.pluck(:id)
15
+ count = filtered_ids.size
16
+
17
+ # order a potentially large set of ids
18
+ ordered_ids = order_for_search(filtered_ids, ordered_search_ids, with_ids: true)
19
+ ordered_paginated_ids = paginate_array(ordered_ids)
20
+
21
+ scope = scope.unscoped.where(id: ordered_paginated_ids)
22
+ # not using `evaluate_scope` because we are already instantiating
23
+ # a scope based on ids
24
+ primary_models = scope.to_a
25
+
26
+ # Once hydrated, a page worth of models needs to be reordered
27
+ # due to the `scope.unscoped.where(id: ...` clobbering our ordering
28
+ primary_models = order_for_search(primary_models, ordered_paginated_ids)
29
+ end
12
30
 
13
31
  [primary_models, count]
14
32
  end
@@ -16,10 +34,8 @@ module Brainstem
16
34
  private
17
35
 
18
36
  def run_search(scope, includes)
19
- sort_name, direction = @options[:primary_presenter].calculate_sort_name_and_direction @options[:params]
20
37
  search_options = HashWithIndifferentAccess.new(
21
38
  include: includes,
22
- order: { sort_order: sort_name, direction: direction },
23
39
  limit: @options[:default_max_filter_and_search_page],
24
40
  offset: 0
25
41
  )
@@ -28,23 +44,28 @@ module Brainstem
28
44
 
29
45
  result_ids, _ = @options[:primary_presenter].run_search(@options[:params][:search], search_options)
30
46
  if result_ids
31
- result_ids
47
+ resulting_scope = scope.where(id: result_ids)
48
+ [resulting_scope, result_ids]
32
49
  else
33
50
  raise(SearchUnavailableError, 'Search is currently unavailable')
34
51
  end
35
52
  end
36
53
 
37
- def paginate(scope)
38
- if @options[:params][:limit].present? && @options[:params][:offset].present?
39
- limit = calculate_limit
40
- offset = calculate_offset
41
- else
42
- limit = calculate_per_page
43
- offset = limit * (calculate_page - 1)
44
- end
54
+ def ordering?
55
+ sort_name = @options[:params][:order].to_s.split(":").first
56
+ sort_orders = @options[:primary_presenter].configuration[:sort_orders]
57
+ sort_name.present? && sort_orders && sort_orders[sort_name].present?
58
+ end
45
59
 
60
+ def paginate(scope)
61
+ limit, offset = calculate_limit_and_offset
46
62
  scope.limit(limit).offset(offset).distinct
47
63
  end
64
+
65
+ def paginate_array(array)
66
+ limit, offset = calculate_limit_and_offset
67
+ array.drop(offset).first(limit) # do we need to uniq this?
68
+ end
48
69
  end
49
70
  end
50
71
  end
@@ -68,14 +68,7 @@ module Brainstem
68
68
  end
69
69
 
70
70
  def paginate(scope)
71
- if @options[:params][:limit].present? && @options[:params][:offset].present?
72
- limit = calculate_limit
73
- offset = calculate_offset
74
- else
75
- limit = calculate_per_page
76
- offset = limit * (calculate_page - 1)
77
- end
78
-
71
+ limit, offset = calculate_limit_and_offset
79
72
  [scope.limit(limit).offset(offset).distinct, scope.select("distinct #{scope.connection.quote_table_name @options[:table_name]}.id").count]
80
73
  end
81
74
 
@@ -83,21 +76,6 @@ module Brainstem
83
76
  ids = (only || "").split(",").select {|id| id =~ /\A\d+\z/}.uniq
84
77
  [scope.where(:id => ids), scope.where(:id => ids).count]
85
78
  end
86
-
87
- def order_for_search(records, ordered_search_ids)
88
- ids_to_position = {}
89
- ordered_records = []
90
-
91
- ordered_search_ids.each_with_index do |id, index|
92
- ids_to_position[id] = index
93
- end
94
-
95
- records.each do |record|
96
- ordered_records[ids_to_position[record.id]] = record
97
- end
98
-
99
- ordered_records.compact
100
- end
101
79
  end
102
80
  end
103
81
  end
@@ -1,3 +1,3 @@
1
1
  module Brainstem
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -10,7 +10,7 @@ describe Brainstem::PresenterCollection do
10
10
  let(:jane) { User.where(:username => "jane").first }
11
11
 
12
12
  describe "#presenting" do
13
- describe "#pagination" do
13
+ describe "pagination" do
14
14
  before do
15
15
  @presenter_collection.default_per_page = 2
16
16
  @presenter_collection.default_max_per_page = 3
@@ -159,6 +159,64 @@ describe Brainstem::PresenterCollection do
159
159
  expect(result['count']).to eq(Workspace.count)
160
160
  end
161
161
  end
162
+
163
+ describe "meta keys" do
164
+ let(:max_per_page) { nil }
165
+ let(:meta_keys) { result["meta"] }
166
+ let(:result) { @presenter_collection.presenting("workspaces", params: params, max_per_page: max_per_page) { Workspace.unscoped } }
167
+
168
+ describe "count" do
169
+ let(:params) { {} }
170
+
171
+ it "includes the count" do
172
+ expect(meta_keys["count"]).to eq(6)
173
+ end
174
+ end
175
+
176
+ describe "page_number" do
177
+ describe "when page is provided" do
178
+ let(:params) { { page: 2 } }
179
+
180
+ it "indicates the provided page" do
181
+ expect(meta_keys["page_number"]).to eq(2)
182
+ end
183
+ end
184
+
185
+ describe "when page is not profided" do
186
+ let(:params) { {} }
187
+
188
+ it "indicates the first page" do
189
+ expect(meta_keys["page_number"]).to eq(1)
190
+ end
191
+ end
192
+ end
193
+
194
+ describe "page_count" do
195
+ describe "when per_page is provided" do
196
+ let(:params) { { per_page: 4 } }
197
+
198
+ it "calculates the correct page count" do
199
+ expect(meta_keys["page_count"]).to eq(2)
200
+ end
201
+ end
202
+
203
+ describe "when per_page is not provided" do
204
+ let(:params) { {} }
205
+
206
+ it "calculates the correct page count based on the default page size" do
207
+ expect(meta_keys["page_count"]).to eq(3)
208
+ end
209
+ end
210
+ end
211
+
212
+ describe "page_size" do
213
+ let(:params) { {} }
214
+
215
+ it "calculates the correct page size" do
216
+ expect(meta_keys["page_count"]).to eq(3)
217
+ end
218
+ end
219
+ end
162
220
  end
163
221
 
164
222
  describe 'strategies' do
@@ -256,7 +314,7 @@ describe Brainstem::PresenterCollection do
256
314
  describe "the 'results' top level key" do
257
315
  it "comes back with an explicit list of the matching results" do
258
316
  structure = @presenter_collection.presenting("workspaces", :params => { :include => "tasks" }, :max_per_page => 2) { Workspace.where(:id => 1) }
259
- expect(structure.keys).to match_array %w[workspaces tasks count results]
317
+ expect(structure.keys).to match_array %w[workspaces tasks count results meta]
260
318
  expect(structure['results']).to eq(Workspace.where(:id => 1).limit(2).map {|w| { 'key' => 'workspaces', 'id' => w.id.to_s } })
261
319
  expect(structure['workspaces'].keys).to eq(%w[1])
262
320
  end
@@ -265,10 +323,10 @@ describe Brainstem::PresenterCollection do
265
323
  describe "includes" do
266
324
  it "reads allowed includes from the presenter" do
267
325
  result = @presenter_collection.presenting("workspaces", :params => { :include => "drop table,tasks,users" }) { Workspace.unscoped }
268
- expect(result.keys).to match_array %w[count workspaces tasks results]
326
+ expect(result.keys).to match_array %w[count meta workspaces tasks results]
269
327
 
270
328
  result = @presenter_collection.presenting("workspaces", :params => { :include => "foo,tasks,lead_user" }) { Workspace.unscoped }
271
- expect(result.keys).to match_array %w[count workspaces tasks users results]
329
+ expect(result.keys).to match_array %w[count meta workspaces tasks users results]
272
330
  end
273
331
 
274
332
  it "defaults to not include any allowed includes" do
@@ -304,7 +362,7 @@ describe Brainstem::PresenterCollection do
304
362
  result = @presenter_collection.presenting("tasks", :params => { :include => "workspace" }, :max_per_page => 2) { Task.where(:id => t.id) }
305
363
  expect(result['tasks'].keys).to eq([ t.id.to_s ])
306
364
  expect(result['workspaces']).to eq({})
307
- expect(result.keys).to match_array %w[tasks workspaces count results]
365
+ expect(result.keys).to match_array %w[tasks workspaces count meta results]
308
366
  end
309
367
 
310
368
  context 'when including something of the same type as the primary model' do
@@ -911,7 +969,7 @@ describe Brainstem::PresenterCollection do
911
969
  context "when there is no sort provided" do
912
970
  it "returns an empty array when there are no objects" do
913
971
  result = @presenter_collection.presenting("workspaces") { Workspace.where(:id => nil) }
914
- expect(result).to eq('count' => 0, 'workspaces' => {}, 'results' => [])
972
+ expect(result).to eq('count' => 0, 'meta' => { 'count' => 0, 'page_count' => 0, 'page_number' => 0, 'page_size' => 20 }, 'workspaces' => {}, 'results' => [])
915
973
  end
916
974
 
917
975
  it "falls back to the object's sort order when nothing is provided" do
@@ -966,7 +1024,7 @@ describe Brainstem::PresenterCollection do
966
1024
 
967
1025
  result = @presenter_collection.presenting("workspaces", :params => { :order => "description:drop table" }) { Workspace.where("id is not null") }
968
1026
  expect(last_direction).to eq('asc')
969
- expect(result.keys).to match_array %w[count workspaces results]
1027
+ expect(result.keys).to match_array %w[count meta workspaces results]
970
1028
 
971
1029
  result = @presenter_collection.presenting("workspaces", :params => { :order => "description:;;hacker;;" }) { Workspace.where("id is not null") }
972
1030
  expect(last_direction).to eq('asc')
@@ -1087,7 +1145,8 @@ describe Brainstem::PresenterCollection do
1087
1145
 
1088
1146
  describe '#structure_response' do
1089
1147
  let(:options) { {params: {}, primary_presenter: @presenter_collection.for!(Workspace) } }
1090
- let(:response_body) { @presenter_collection.structure_response(Workspace, Workspace.all, 17, options) }
1148
+ let(:response_body) { @presenter_collection.structure_response(Workspace, Workspace.all, strategy, 17, options) }
1149
+ let(:strategy) { OpenStruct.new(calculate_per_page: 25) }
1091
1150
 
1092
1151
  it 'has a count' do
1093
1152
  expect(response_body['count']).to eq(17)
@@ -2,11 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  describe Brainstem::QueryStrategies::FilterAndSearch do
4
4
  let(:bob) { User.find_by_username('bob') }
5
+ let(:jane) { User.find_by_username('jane') }
5
6
 
6
- let(:params) { {
7
+ let(:default_params) { {
7
8
  "owned_by" => bob.id.to_s,
8
- per_page: 7,
9
- page: 1,
10
9
  search: 'toot, otto, toot',
11
10
  } }
12
11
 
@@ -19,28 +18,150 @@ describe Brainstem::QueryStrategies::FilterAndSearch do
19
18
  params: params
20
19
  } }
21
20
 
21
+ it_behaves_like Brainstem::QueryStrategies::BaseStrategy
22
+
22
23
  describe '#execute' do
24
+ def run_query
25
+ described_class.new(options).execute(Cheese.all)
26
+ end
27
+
28
+ let(:owned_by_bob) { Cheese.owned_by(bob.id)}
29
+ let(:owned_by_jane) { Cheese.owned_by(jane.id)}
30
+
23
31
  before do
24
- CheesePresenter.search do |string, options|
25
- [[2,3,4,5,6,8,9,10,11,12], 11]
32
+ @search_results = search_results = Cheese.all.pluck(:id).shuffle
33
+
34
+ CheesePresenter.search do
35
+ [search_results, search_results.count]
26
36
  end
27
37
 
28
38
  CheesePresenter.filter(:owned_by) { |scope, user_id| scope.owned_by(user_id.to_i) }
29
39
  CheesePresenter.sort_order(:id) { |scope, direction| scope.order("cheeses.id #{direction}") }
30
40
  end
31
41
 
32
- it 'takes the intersection of the search and filter results' do
33
- results, count = described_class.new(options).execute(Cheese.unscoped)
34
- expect(count).to eq(8)
35
- expect(results.map(&:id)).to eq([2,3,4,5,8,10,11])
42
+ context 'when an order is specified' do
43
+ let(:order) { 'id:asc' }
44
+ let(:params) { default_params.merge({ order: order })}
45
+ let(:expected_ordered_ids) { owned_by_bob.order("cheeses.id ASC").pluck(:id) }
46
+
47
+ it 'returns the filtered, ordered search results' do
48
+ results, count = run_query
49
+ expect(count).to eq(owned_by_bob.count)
50
+ expect(results.map(&:id)).to eq(expected_ordered_ids)
51
+ end
52
+
53
+ context 'with limit and offset params' do
54
+ let(:limit) { 2 }
55
+ let(:offset) { 4 }
56
+ let(:params) { default_params.merge({ order: order, limit: limit, offset: offset })}
57
+ let(:expected_paginated_ids) { expected_ordered_ids.drop(offset).first(limit) }
58
+
59
+ it 'returns the filtered, ordered, paginated results' do
60
+ results, count = run_query
61
+ expect(count).to eq(owned_by_bob.count)
62
+ expect(results.map(&:id)).to eq(expected_paginated_ids)
63
+ end
64
+ end
65
+
66
+ context 'with page and per_page params' do
67
+ let(:page) { 2 }
68
+ let(:per_page) { 3 }
69
+ let(:params) { default_params.merge({ order: order, page: page, per_page: per_page })}
70
+ let(:expected_paginated_ids) { expected_ordered_ids.drop(per_page).first(per_page) }
71
+
72
+ it 'returns the filtered, ordered, paginated results' do
73
+ results, count = run_query
74
+ expect(count).to eq(owned_by_bob.count)
75
+ expect(results.map(&:id)).to eq(expected_paginated_ids)
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'when no order is specified' do
81
+ let(:params) { default_params }
82
+ let(:expected_ordered_ids) { @search_results - owned_by_jane.pluck(:id) }
83
+
84
+ before do
85
+ expect(params[:order]).not_to be_present
86
+ end
87
+
88
+ it 'returns the filtered results ordered by search' do
89
+ results, count = run_query
90
+ expect(count).to eq(owned_by_bob.count)
91
+ expect(results.map(&:id)).to eq(expected_ordered_ids)
92
+ end
93
+
94
+ it 'only does two database queries' do
95
+ expect { run_query }.to make_database_queries(count: 2)
96
+ end
97
+
98
+ context 'with limit and offset params' do
99
+ let(:limit) { 2 }
100
+ let(:offset) { 4 }
101
+ let(:params) { default_params.merge({ limit: limit, offset: offset })}
102
+ let(:expected_paginated_ids) { expected_ordered_ids.drop(offset).first(limit) }
103
+
104
+ it 'returns the filtered, ordered, paginated results' do
105
+ results, count = run_query
106
+ expect(count).to eq(owned_by_bob.count)
107
+ expect(results.map(&:id)).to eq(expected_paginated_ids)
108
+ end
109
+ end
110
+
111
+ context 'with page and per_page params' do
112
+ let(:page) { 2 }
113
+ let(:per_page) { 3 }
114
+ let(:params) { default_params.merge({ page: page, per_page: per_page })}
115
+ let(:expected_paginated_ids) { expected_ordered_ids.drop(per_page).first(per_page) }
116
+
117
+ it 'returns the filtered, ordered, paginated results' do
118
+ results, count = run_query
119
+ expect(count).to eq(owned_by_bob.count)
120
+ expect(results.map(&:id)).to eq(expected_paginated_ids)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ describe '#ordering?' do
127
+ context 'when the order param is passed' do
128
+ let(:params) { default_params.merge({ order: 'canadianness' })}
129
+
130
+ context 'and it exists on the presenter' do
131
+ before do
132
+ CheesePresenter.sort_order(:canadianness) { |scope, direction| scope.order("cheeses.hockey #{direction}") }
133
+ expect(CheesePresenter.configuration[:sort_orders][:canadianness]).to be_present
134
+ end
135
+
136
+ it 'returns true' do
137
+ query_strat = described_class.new(options)
138
+ expect(query_strat.send(:ordering?)).to eq(true)
139
+ end
140
+ end
141
+
142
+ context 'and it does not exist on the presenter' do
143
+ before do
144
+ expect(CheesePresenter.configuration[:sort_orders][:canadianness]).not_to be_present
145
+ end
146
+
147
+ it 'returns false' do
148
+ query_strat = described_class.new(options)
149
+ expect(query_strat.send(:ordering?)).to eq(false)
150
+ end
151
+ end
36
152
  end
37
153
 
38
- it "applies ordering to the scope" do
39
- options[:params]["order"] = 'id:desc'
40
- proxy.instance_of(Brainstem::Presenter).apply_ordering_to_scope(anything, anything).times(1)
41
- results, count = described_class.new(options).execute(Cheese.unscoped)
42
- expect(count).to eq(8)
43
- expect(results.map(&:id)).to eq([12,11,10,8,5,4,3])
154
+ context 'when the order param is not passed' do
155
+ let(:params) { default_params }
156
+
157
+ before do
158
+ expect(params[:order]).not_to be_present
159
+ end
160
+
161
+ it 'returns false' do
162
+ query_strat = described_class.new(options)
163
+ expect(query_strat.send(:ordering?)).to eq(false)
164
+ end
44
165
  end
45
166
  end
46
167
  end
@@ -3,6 +3,8 @@ require 'spec_helper'
3
3
  # The functionality of this is mainly tested in more integration-y tests in presenter_collection_spec.rb
4
4
 
5
5
  describe Brainstem::QueryStrategies::FilterOrSearch do
6
+ it_behaves_like Brainstem::QueryStrategies::BaseStrategy
7
+
6
8
  describe '#execute' do
7
9
  context 'we are searching' do
8
10
  let(:subject) { described_class.new(@options) }
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ shared_examples_for Brainstem::QueryStrategies::BaseStrategy do
4
+ let(:strategy) { described_class.new(options) }
5
+
6
+ describe "#calculate_per_page" do
7
+ let(:result) { strategy.calculate_per_page }
8
+
9
+ [
10
+ # per_page default_per_page max_per_page default_max_per_page expected situation used
11
+ [ 10, 20, 100, 200, 10, "per page < max", "the per page"],
12
+ [ nil, 20, 100, 200, 20, "no per page and default < max", "the default"],
13
+ [ nil, 200, 100, 200, 100, "no per page and default > max", "the max"],
14
+ [ 150, 20, 100, 200, 100, "per page > max", "the max"],
15
+ [ 150, 20, nil, 200, 150, "no max and per page < default max", "the per page"],
16
+ [ 250, 20, nil, 200, 200, "no max and per page > default max", "the default max"],
17
+ ].each do |per_page, default_per_page, max_per_page, default_max_per_page, expected, situation, used|
18
+ describe "when #{situation}" do
19
+ let(:options) {
20
+ {
21
+ params: {
22
+ per_page: per_page,
23
+ },
24
+ default_per_page: default_per_page,
25
+ default_max_per_page: default_max_per_page,
26
+ max_per_page: max_per_page,
27
+ }
28
+ }
29
+
30
+ it "uses #{used}" do
31
+ expect(result).to eq(expected)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -6,6 +6,7 @@ require 'sqlite3'
6
6
  require 'database_cleaner'
7
7
  require 'pry'
8
8
  require 'pry-nav'
9
+ require 'db-query-matchers'
9
10
 
10
11
  require 'brainstem'
11
12
  require_relative 'spec_helpers/schema'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainstem
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mavenlink
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-20 00:00:00.000000000 Z
11
+ date: 2018-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: db-query-matchers
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: Brainstem allows you to create rich API presenters that know how to filter,
168
182
  sort, and include associations.
169
183
  email:
@@ -291,6 +305,7 @@ files:
291
305
  - spec/brainstem_spec.rb
292
306
  - spec/dummy/rails.rb
293
307
  - spec/shared/atlas_taker.rb
308
+ - spec/shared/base_strategy.rb
294
309
  - spec/shared/formattable.rb
295
310
  - spec/spec_helper.rb
296
311
  - spec/spec_helpers/db.rb
@@ -317,7 +332,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
317
332
  version: '0'
318
333
  requirements: []
319
334
  rubyforge_project:
320
- rubygems_version: 2.5.2
335
+ rubygems_version: 2.6.13
321
336
  signing_key:
322
337
  specification_version: 4
323
338
  summary: ActiveRecord presenters with a rich request API
@@ -368,6 +383,7 @@ test_files:
368
383
  - spec/brainstem_spec.rb
369
384
  - spec/dummy/rails.rb
370
385
  - spec/shared/atlas_taker.rb
386
+ - spec/shared/base_strategy.rb
371
387
  - spec/shared/formattable.rb
372
388
  - spec/spec_helper.rb
373
389
  - spec/spec_helpers/db.rb