brainstem 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Brainstem
2
+ class Presenter
3
+ # This constant stores an array of classes that we will treat as times.
4
+ # Unfortunately, ActiveSupport::TimeWithZone does not descend from
5
+ # Time, so we put them into this array for later use.
6
+ TIME_CLASSES = [Time]
7
+
8
+ begin
9
+ require 'active_support/time_with_zone'
10
+ TIME_CLASSES << ActiveSupport::TimeWithZone
11
+ rescue LoadError
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Brainstem
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/presenters'
3
+
4
+ describe Brainstem::ControllerMethods do
5
+ class FakeController
6
+ include Brainstem::ControllerMethods
7
+
8
+ attr_accessor :call_results
9
+
10
+ def params
11
+ { :a => :b }
12
+ end
13
+ end
14
+
15
+ before do
16
+ UserPresenter.presents User
17
+ TaskPresenter.presents Task
18
+ WorkspacePresenter.presents Workspace
19
+ PostPresenter.presents Post
20
+ end
21
+
22
+ describe "#present_object" do
23
+ before do
24
+ @controller = FakeController.new
25
+ end
26
+
27
+ describe "calling #present with sensible params" do
28
+ before do
29
+ def @controller.present(klass, options)
30
+ @call_results = { :klass => klass, :options => options, :block_result => yield }
31
+ end
32
+ end
33
+
34
+ it "works with arrays of ActiveRecord objects" do
35
+ @controller.present_object([Workspace.find(1), Workspace.find(3)])
36
+ @controller.call_results[:klass].should == Workspace
37
+ @controller.call_results[:options][:as].should == "workspaces"
38
+ @controller.call_results[:block_result].pluck(:id).should == [1, 3]
39
+ end
40
+
41
+ it "works with a Relation" do
42
+ @controller.present_object(Workspace.owned_by(1))
43
+ @controller.call_results[:klass].should == Workspace
44
+ @controller.call_results[:options][:as].should == "workspaces"
45
+ @controller.call_results[:block_result].pluck(:id).should == [1, 2, 3, 4]
46
+ end
47
+
48
+ it "works with singleton objects" do
49
+ @controller.present_object(Workspace.find(1))
50
+ @controller.call_results[:klass].should == Workspace
51
+ @controller.call_results[:options][:as].should == "workspaces"
52
+ @controller.call_results[:block_result].pluck(:id).should == [1]
53
+ end
54
+
55
+ it "accepts a key map" do
56
+ @controller.present_object(Workspace.find(1), :key_map => { "Workspace" => "your_workspaces" })
57
+ @controller.call_results[:klass].should == Workspace
58
+ @controller.call_results[:options][:as].should == "your_workspaces"
59
+ @controller.call_results[:block_result].pluck(:id).should == [1]
60
+ end
61
+
62
+ it "passes :apply_default_filters => false to the PresenterCollection so that filters are not applied by default" do
63
+ @controller.present_object(Workspace.find(1))
64
+ @controller.call_results[:options][:apply_default_filters].should == false
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,486 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/presenters'
3
+
4
+ describe Brainstem::PresenterCollection do
5
+ before do
6
+ UserPresenter.presents User
7
+ TaskPresenter.presents Task
8
+ WorkspacePresenter.presents Workspace
9
+ PostPresenter.presents Post
10
+ @presenter_collection = Brainstem.presenter_collection
11
+ end
12
+
13
+ describe "#presenting" do
14
+ describe "#pagination" do
15
+ before do
16
+ @presenter_collection.default_per_page = 2
17
+ @presenter_collection.default_max_per_page = 3
18
+ end
19
+
20
+ it "has a global per_page default" do
21
+ @presenter_collection.presenting("workspaces") { Workspace.order('id desc') }[:workspaces].length.should == 2
22
+ end
23
+
24
+ it "will not accept a per_page less than 1" do
25
+ @presenter_collection.presenting("workspaces", :params => { :per_page => 0 }) { Workspace.order('id desc') }[:workspaces].length.should == 2
26
+ end
27
+
28
+ it "will accept strings" do
29
+ struct = @presenter_collection.presenting("workspaces", :params => { :per_page => "1", :page => "2" }) { Workspace.order('id desc') }
30
+ struct[:results].first[:id].should == Workspace.order('id desc')[1].id.to_s
31
+ end
32
+
33
+ it "has a global max_per_page default" do
34
+ @presenter_collection.presenting("workspaces", :params => { :per_page => 5 }) { Workspace.order('id desc') }[:workspaces].length.should == 3
35
+ end
36
+
37
+ it "takes a configurable default page size and max page size" do
38
+ @presenter_collection.presenting("workspaces", :params => { :per_page => 5 }, :max_per_page => 4) { Workspace.order('id desc') }[:workspaces].length.should == 4
39
+ end
40
+
41
+ describe "limits and offsets" do
42
+ it "honors the user's requested page size and page and returns counts" do
43
+ result = @presenter_collection.presenting("workspaces", :params => { :per_page => 1, :page => 2 }) { Workspace.order('id desc') }[:results]
44
+ result.length.should == 1
45
+ result.first[:id].should == Workspace.order('id desc')[1].id.to_s
46
+
47
+ result = @presenter_collection.presenting("workspaces", :params => { :per_page => 2, :page => 2 }) { Workspace.order('id desc') }[:results]
48
+ result.length.should == 2
49
+ result.map { |m| m[:id] }.should == Workspace.order('id desc')[2..3].map(&:id).map(&:to_s)
50
+ end
51
+
52
+ it "defaults to 1 if the page number is less than 1" do
53
+ result = @presenter_collection.presenting("workspaces", :params => { :per_page => 1, :page => 0 }) { Workspace.order('id desc') }[:results]
54
+ result.length.should == 1
55
+ result.first[:id].should == Workspace.order('id desc')[0].id.to_s
56
+ end
57
+ end
58
+
59
+ describe "counts" do
60
+ before do
61
+ @presenter_collection.default_per_page = 500
62
+ @presenter_collection.default_max_per_page = 500
63
+ end
64
+
65
+ it "returns the unique count by model id" do
66
+ result = @presenter_collection.presenting("workspaces", :params => { :per_page => 2, :page => 1 }) { Workspace.order('id desc') }
67
+ result[:count].should == Workspace.count
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "uses presenters" do
73
+ it "finds presenter by table name string" do
74
+ result = @presenter_collection.presenting("workspaces") { Workspace.order('id desc') }
75
+ result[:workspaces].length.should eq(Workspace.count)
76
+ end
77
+
78
+ it "finds presenter by model name string" do
79
+ result = @presenter_collection.presenting("Workspace") { order('id desc') }
80
+ result[:workspaces].length.should eq(Workspace.count)
81
+ end
82
+
83
+ it "finds presenter by model" do
84
+ result = @presenter_collection.presenting(Workspace) { order('id desc') }
85
+ result[:workspaces].length.should eq(Workspace.count)
86
+ end
87
+
88
+ it "infers the table name from the model" do
89
+ result = @presenter_collection.presenting("not_workspaces", :model => "Workspace", :params => { :per_page => 2, :page => 1 }) { Workspace.order('id desc') }
90
+ result[:not_workspaces].should_not be_empty
91
+ result[:count].should == Workspace.count
92
+ end
93
+ end
94
+
95
+ describe "the 'results' top level key" do
96
+ it "comes back with an explicit list of the matching results" do
97
+ structure = @presenter_collection.presenting("workspaces", :params => { :include => "tasks" }, :max_per_page => 2) { Workspace.where(:id => 1) }
98
+ structure.keys.should =~ [:workspaces, :tasks, :count, :results]
99
+ structure[:results].should == Workspace.where(:id => 1).limit(2).map {|w| { :key => "workspaces", :id => w.id.to_s } }
100
+ structure[:workspaces].keys.should == %w[1]
101
+ end
102
+ end
103
+
104
+ describe "includes" do
105
+ it "reads allowed includes from the presenter" do
106
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "drop table,tasks,users" }) { Workspace.order('id desc') }
107
+ result.keys.should =~ [:count, :workspaces, :tasks, :results]
108
+
109
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "foo,tasks,lead_user" }) { Workspace.order('id desc') }
110
+ result.keys.should =~ [:count, :workspaces, :tasks, :users, :results]
111
+ end
112
+
113
+ it "allows the allowed includes list to have different json names and association names" do
114
+ result = @presenter_collection.presenting("tasks",
115
+ :params => { :include => "other_tasks" }) { Task.order('id desc') }
116
+ result[:tasks].should be_present
117
+ result[:other_tasks].should be_present
118
+ end
119
+
120
+ it "defaults to not include any allowed includes" do
121
+ tasked_workspace = Task.first
122
+ result = @presenter_collection.presenting("workspaces", :max_per_page => 2) { Workspace.where(:id => tasked_workspace.workspace_id) }
123
+ result[:workspaces].keys.should == [ tasked_workspace.workspace_id.to_s ]
124
+ result[:tasks].should be_nil
125
+ end
126
+
127
+ it "loads has_many associations and returns them when requested" do
128
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "tasks" }, :max_per_page => 2) { Workspace.where(:id => 1) }
129
+ result[:tasks].keys.should =~ Workspace.first.tasks.map(&:id).map(&:to_s)
130
+ result[:workspaces]["1"][:task_ids].should =~ Workspace.first.tasks.map(&:id).map(&:to_s)
131
+ end
132
+
133
+ it "returns appropriate fields" do
134
+ result = @presenter_collection.presenting("workspaces",
135
+ :params => { :include => "tasks" },
136
+ :max_per_page => 2) { Workspace.where(:id => 1) }
137
+ result[:workspaces].values.first.should have_key(:description)
138
+ result[:tasks].values.first.should have_key(:name)
139
+ end
140
+
141
+ it "loads belongs_tos and returns them when requested" do
142
+ result = @presenter_collection.presenting("tasks", :params => { :include => "workspace" }, :max_per_page => 2) { Task.where(:id => 1) }
143
+ result[:workspaces].keys.should == %w[1]
144
+ end
145
+
146
+ it "doesn't return nils when belong_tos are missing" do
147
+ t = Task.first
148
+ t.update_attribute :workspace, nil
149
+ t.reload.workspace.should be_nil
150
+ result = @presenter_collection.presenting("tasks", :params => { :include => "workspace" }, :max_per_page => 2) { Task.where(:id => t.id) }
151
+ result[:tasks].keys.should == [ t.id.to_s ]
152
+ result[:workspaces].should eq({})
153
+ result.keys.should =~ [:tasks, :workspaces, :count, :results]
154
+ end
155
+
156
+ it "returns sensible data when including something of the same type as the primary model" do
157
+ result = @presenter_collection.presenting("tasks", :params => { :include => "sub_tasks" }) { Task.where(:id => 2) }
158
+ sub_task_ids = Task.find(2).sub_tasks.map(&:id).map(&:to_s)
159
+ result[:tasks].keys.should =~ sub_task_ids + ["2"]
160
+ result[:tasks]["2"][:sub_task_ids].should == sub_task_ids # The primary should have a sub_story_ids array.
161
+ result[:tasks][sub_task_ids.first][:sub_task_ids].should_not be_present # Sub stories should not have a sub_story_ids array.
162
+ end
163
+
164
+ it "includes requested includes even when all records are filtered" do
165
+ result = @presenter_collection.presenting("workspaces", :params => { :only => "not an id", :include => "not an include,tasks" }) { Workspace.order("id desc") }
166
+ result[:workspaces].length.should == 0
167
+ result[:tasks].length.should == 0
168
+ end
169
+
170
+ it "includes requested includes even when the scope has no records" do
171
+ Workspace.where(:id => 123456789).should be_empty
172
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "not an include,tasks" }) { Workspace.where(:id => 123456789) }
173
+ result[:workspaces].length.should == 0
174
+ result[:tasks].length.should == 0
175
+ end
176
+
177
+ it "preloads associations when they are full model-level associations" do
178
+ # Here, primary_maven is a method on Workspace, not a true association.
179
+ mock(ActiveRecord::Associations::Preloader).new(anything, [:tasks]) { mock!.run }
180
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "tasks" }) { Workspace.order('id desc') }
181
+ result[:tasks].length.should > 0
182
+ end
183
+
184
+ it "works with model methods that load records (but without preloading)" do
185
+ result = @presenter_collection.presenting("workspaces", :params => { :include => "lead_user" }) { Workspace.order('id desc') }
186
+ result[:workspaces][Workspace.first.id.to_s].should be_present
187
+ result[:users][Workspace.first.lead_user.id.to_s].should be_present
188
+ end
189
+
190
+ describe "polymorphic associations" do
191
+ it "works with polymorphic associations" do
192
+ result = @presenter_collection.presenting("posts", :params => { :include => "subject" }) { Post.order('id desc') }
193
+ result[:posts][Post.first.id.to_s].should be_present
194
+ result[:workspaces][Workspace.first.id.to_s].should be_present
195
+ result[:tasks][Task.first.id.to_s].should be_present
196
+ end
197
+
198
+ it "does not return an empty hash when none are found" do
199
+ result = @presenter_collection.presenting("posts", :params => { :include => "subject" }) { Post.where(:id => nil) }
200
+ result.should have_key(:posts)
201
+ result.should_not have_key(:workspaces)
202
+ result.should_not have_key(:tasks)
203
+ end
204
+ end
205
+ end
206
+
207
+ describe "handling of only" do
208
+ it "accepts params[:only] as a list of ids to limit to" do
209
+ result = @presenter_collection.presenting("workspaces", :params => { :only => Workspace.limit(2).pluck(:id).join(",") }) { Workspace.order("id desc") }
210
+ result[:workspaces].keys.should match_array(Workspace.limit(2).pluck(:id).map(&:to_s))
211
+ end
212
+
213
+ it "does not paginate only requests" do
214
+ dont_allow(@presenter_collection).paginate
215
+ @presenter_collection.presenting("workspaces", :params => { :only => Workspace.limit(2).pluck(:id).join(",") }) { Workspace.order("id desc") }
216
+ end
217
+
218
+ it "escapes ids" do
219
+ result = @presenter_collection.presenting("workspaces", :params => { :only => "#{Workspace.first.id}foo,;drop tables;,#{Workspace.first.id}" }) { Workspace.order("id desc") }
220
+ result[:workspaces].length.should == 1
221
+ end
222
+
223
+ it "only runs when it receives ids" do
224
+ result = @presenter_collection.presenting("workspaces", :params => { :only => "" }) { Workspace.order("id desc") }
225
+ result[:workspaces].length.should > 1
226
+
227
+ result = @presenter_collection.presenting("workspaces", :params => { :only => "1" }) { Workspace.order("id desc") }
228
+ result[:workspaces].length.should <= 1
229
+ end
230
+ end
231
+
232
+ describe "filters" do
233
+ before do
234
+ WorkspacePresenter.filter(:owned_by) { |scope, user_id| scope.owned_by(user_id.to_i) }
235
+ WorkspacePresenter.filter(:title) { |scope, title| scope.where(:title => title) }
236
+ end
237
+
238
+ let(:bob) { User.where(:username => "bob").first }
239
+ let(:bob_workspaces_ids) { bob.workspaces.map(&:id) }
240
+
241
+ it "limits records to those matching given filters" do
242
+ result = @presenter_collection.presenting("workspaces", :params => { :owned_by => bob.id.to_s }) { Workspace.order("id desc") } # hit the API, filtering on owned_by:bob
243
+ result[:workspaces].should be_present
244
+ result[:workspaces].keys.all? {|id| bob_workspaces_ids.map(&:to_s).include?(id) }.should be_true # all of the returned workspaces should contain bob
245
+ end
246
+
247
+ it "returns all records if filters are not given" do
248
+ result = @presenter_collection.presenting("workspaces") { Workspace.order("id desc") } # hit the API again, this time not filtering on anything
249
+ result[:workspaces].keys.all? {|id| bob_workspaces_ids.map(&:to_s).include?(id) }.should be_false # the returned workspaces no longer all contain bob
250
+ end
251
+
252
+ it "ignores unknown filters" do
253
+ result = @presenter_collection.presenting("workspaces", :params => { :wut => "is this?" }) { Workspace.order("id desc") }
254
+ result[:workspaces].keys.all? {|id| bob_workspaces_ids.map(&:to_s).include?(id) }.should be_false
255
+ end
256
+
257
+ it "limits records to those matching all given filters" do
258
+ result = @presenter_collection.presenting("workspaces", :params => { :owned_by => bob.id.to_s, :title => "bob workspace 1" }) { Workspace.order("id desc") } # try two filters
259
+ result[:results].first[:id].should == Workspace.where(:title => "bob workspace 1").first.id.to_s
260
+ end
261
+
262
+ it "converts boolean parameters from strings to booleans" do
263
+ WorkspacePresenter.filter(:owned_by_bob) { |scope, boolean| boolean ? scope.where(:user_id => bob.id) : scope }
264
+ result = @presenter_collection.presenting("workspaces", :params => { :owned_by_bob => "false" }) { Workspace.scoped }
265
+ result[:workspaces].values.find { |workspace| workspace[:title].include?("jane") }.should be
266
+ end
267
+
268
+ it "ensures arguments are strings" do
269
+ WorkspacePresenter.filter(:owned_by_bob) { |scope, string| string.should be_a(String); scope }
270
+ result = @presenter_collection.presenting("workspaces", :params => { :owned_by_bob => [1, 2] }) { Workspace.scoped }
271
+ end
272
+
273
+ it "allows filters to be called with false as an argument" do
274
+ WorkspacePresenter.filter(:nothing) { |scope, bool| bool ? scope.where(:id => nil) : scope }
275
+ result = @presenter_collection.presenting("workspaces", :params => { :nothing => "true" }) { Workspace.scoped }
276
+ result[:workspaces].length.should eq(0)
277
+ result = @presenter_collection.presenting("workspaces", :params => { :nothing => "false" }) { Workspace.scoped }
278
+ result[:workspaces].length.should_not eq(0)
279
+ end
280
+
281
+ it "passes colon separated params through as a string" do
282
+ WorkspacePresenter.filter(:between) { |scope, a_and_b|
283
+ a, b = a_and_b.split(':')
284
+ a.should == "1"
285
+ b.should == "10"
286
+ scope
287
+ }
288
+
289
+ @presenter_collection.presenting("workspaces", :params => { :between => "1:10" }) { Workspace.scoped }
290
+ end
291
+
292
+ context "with defaults" do
293
+ before do
294
+ WorkspacePresenter.filter(:owner, :default => bob.id) { |scope, id| scope.owned_by(id) }
295
+ end
296
+
297
+ let(:jane) { User.where(:username => "jane").first }
298
+
299
+ it "applies the filter when it is not requested" do
300
+ result = @presenter_collection.presenting("workspaces") { Workspace.order('id desc') }
301
+ result[:workspaces].keys.should match_array(bob.workspaces.map(&:id).map(&:to_s))
302
+ end
303
+
304
+ it "allows falsy defaults" do
305
+ WorkspacePresenter.filter(:include_early_workspaces, :default => false) { |scope, bool| bool ? scope : scope.where("id > 3") }
306
+ result = @presenter_collection.presenting("workspaces") { Workspace.unscoped }
307
+ result[:workspaces]["2"].should_not be_present
308
+ result = @presenter_collection.presenting("workspaces", :params => { :include_early_workspaces => "true" }) { Workspace.unscoped }
309
+ result[:workspaces]["2"].should be_present
310
+ end
311
+
312
+ it "allows defaults to be skipped if :apply_default_filters is false" do
313
+ WorkspacePresenter.filter(:include_early_workspaces, :default => false) { |scope, bool| bool ? scope : scope.where("id > 3") }
314
+ result = @presenter_collection.presenting("workspaces", :apply_default_filters => true) { Workspace.unscoped }
315
+ result[:workspaces]["2"].should_not be_present
316
+ result = @presenter_collection.presenting("workspaces", :apply_default_filters => false) { Workspace.unscoped }
317
+ result[:workspaces]["2"].should be_present
318
+ end
319
+
320
+ it "allows the default value to be overridden" do
321
+ result = @presenter_collection.presenting("workspaces", :params => { :owner => jane.id.to_s }) { Workspace.order('id desc') }
322
+ result[:workspaces].keys.should match_array(jane.workspaces.map(&:id).map(&:to_s))
323
+ end
324
+ end
325
+
326
+ context "without blocks" do
327
+ let(:bob) { User.where(:username => "bob").first }
328
+ let(:jane) { User.where(:username => "jane").first }
329
+
330
+ before do
331
+ WorkspacePresenter.filter(:owned_by, :default => bob.id)
332
+ WorkspacePresenter.presents("Workspace")
333
+ end
334
+
335
+ it "calls the named scope with default arguments" do
336
+ result = @presenter_collection.presenting("workspaces") { Workspace.scoped }
337
+ result[:workspaces].keys.should eq(bob.workspaces.pluck(:id).map(&:to_s))
338
+ end
339
+
340
+ it "calls the named scope with given arguments" do
341
+ result = @presenter_collection.presenting("workspaces", :params => { :owned_by => jane.id.to_s }) { Workspace.scoped }
342
+ result[:workspaces].keys.should eq(jane.workspaces.pluck(:id).map(&:to_s))
343
+ end
344
+
345
+ it "allows scopes that take no arguments" do
346
+ WorkspacePresenter.filter(:numeric_description)
347
+ result = @presenter_collection.presenting("workspaces") { Workspace.scoped }
348
+ result[:workspaces].keys.should eq(bob.workspaces.pluck(:id).map(&:to_s))
349
+ result = @presenter_collection.presenting("workspaces", :params => { :numeric_description => "true" }) { Workspace.scoped }
350
+ result[:workspaces].keys.should =~ ["2", "4"]
351
+ result = @presenter_collection.presenting("workspaces", :params => { :numeric_description => "fadlse" }) { Workspace.scoped }
352
+ result[:workspaces].keys.should eq(bob.workspaces.pluck(:id).map(&:to_s))
353
+ end
354
+ end
355
+ end
356
+
357
+ describe "search" do
358
+ context "with search method defined" do
359
+ before do
360
+ WorkspacePresenter.search do |string|
361
+ [3,5]
362
+ end
363
+ end
364
+
365
+ context "and a search request is made" do
366
+ it "calls the search method" do
367
+ result = @presenter_collection.presenting("workspaces", :params => { :search => "blah" }) { Workspace.order("id asc") }
368
+ result[:workspaces].keys.should eq(%w[3 5])
369
+ end
370
+ end
371
+
372
+ context "and there is no search request" do
373
+ it "does not call the search method" do
374
+ result = @presenter_collection.presenting("workspaces") { Workspace.order("id asc") }
375
+ result[:workspaces].keys.should eq(Workspace.pluck(:id).map(&:to_s))
376
+ end
377
+ end
378
+ end
379
+
380
+ context "without search method defined" do
381
+ context "and a search request is made" do
382
+ it "returns as if there was no search" do
383
+ result = @presenter_collection.presenting("workspaces", :params => { :search => "blah" }) { Workspace.order("id asc") }
384
+ result[:workspaces].keys.should eq(Workspace.pluck(:id).map(&:to_s))
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ describe "sorting and ordering" do
391
+ context "when there is no sort provided" do
392
+ it "returns an empty array when there are no objects" do
393
+ result = @presenter_collection.presenting("workspaces") { Workspace.where(:id => nil) }
394
+ result.should eq(:count => 0, :workspaces => {}, :results => [])
395
+ end
396
+
397
+ it "falls back to the object's sort order when nothing is provided" do
398
+ result = @presenter_collection.presenting("workspaces") { Workspace.where(:id => [1, 3]) }
399
+ result[:workspaces].keys.should == %w[1 3]
400
+ end
401
+ end
402
+
403
+ it "allows default ordering descending" do
404
+ WorkspacePresenter.sort_order(:description, "workspaces.description")
405
+ WorkspacePresenter.default_sort_order("description:desc")
406
+ result = @presenter_collection.presenting("workspaces") { Workspace.where("id is not null") }
407
+ result[:results].map {|i| result[:workspaces][i[:id]][:description] }.should eq(%w(c b a 3 2 1))
408
+ end
409
+
410
+ it "allows default ordering ascending" do
411
+ WorkspacePresenter.sort_order(:description, "workspaces.description")
412
+ WorkspacePresenter.default_sort_order("description:asc")
413
+ result = @presenter_collection.presenting("workspaces") { Workspace.where("id is not null") }
414
+ result[:results].map {|i| result[:workspaces][i[:id]][:description] }.should eq(%w(1 2 3 a b c))
415
+ end
416
+
417
+ it "applies orders that match the default order" do
418
+ WorkspacePresenter.sort_order(:description, "workspaces.description")
419
+ WorkspacePresenter.default_sort_order("description:desc")
420
+ result = @presenter_collection.presenting("workspaces", :params => { :order => "description:desc"} ) { Workspace.where("id is not null") }
421
+ result[:results].map {|i| result[:workspaces][i[:id]][:description] }.should eq(%w(c b a 3 2 1))
422
+ end
423
+
424
+ it "applies orders that conflict with the default order" do
425
+ WorkspacePresenter.sort_order(:description, "workspaces.description")
426
+ WorkspacePresenter.default_sort_order("description:desc")
427
+ result = @presenter_collection.presenting("workspaces", :params => { :order => "description:asc"} ) { Workspace.where("id is not null") }
428
+ result[:results].map {|i| result[:workspaces][i[:id]][:description] }.should eq(%w(1 2 3 a b c))
429
+ end
430
+
431
+ it "cleans the direction param" do
432
+ result = @presenter_collection.presenting("workspaces", :params => { :order => "updated_at:drop table" }) { Workspace.where("id is not null") }
433
+ result.keys.should =~ [:count, :workspaces, :results]
434
+ end
435
+
436
+ it "can take a proc" do
437
+ WorkspacePresenter.sort_order(:description){ Workspace.order("workspaces.description") }
438
+ WorkspacePresenter.default_sort_order("description:asc")
439
+ result = @presenter_collection.presenting("workspaces") { Workspace.where("id is not null") }
440
+ result[:results].map {|i| result[:workspaces][i[:id]][:description] }.should eq(%w(1 2 3 a b c))
441
+ end
442
+ end
443
+
444
+ describe "the :as param" do
445
+ it "determines the chosen top-level key name" do
446
+ result = @presenter_collection.presenting("workspaces", :as => :my_workspaces) { Workspace.where(:id => 1) }
447
+ result.keys.should eq([:count, :my_workspaces, :results])
448
+ end
449
+ end
450
+ end
451
+
452
+ describe "collection methods" do
453
+ describe "for method" do
454
+ module V1
455
+ class ArrayPresenter < Brainstem::Presenter
456
+ end
457
+ end
458
+
459
+ before do
460
+ V1::ArrayPresenter.presents Array
461
+ end
462
+
463
+ it "returns the presenter for a given class" do
464
+ Brainstem.presenter_collection("v1").for(Array).should be_a(V1::ArrayPresenter)
465
+ end
466
+
467
+ it "returns nil when given nil" do
468
+ Brainstem.presenter_collection("v1").for(nil).should be_nil
469
+ end
470
+
471
+ it "returns nil when a given class has no presenter" do
472
+ Brainstem.presenter_collection("v1").for(String).should be_nil
473
+ end
474
+
475
+ it "uses the default namespace when the passed namespace is nil" do
476
+ Brainstem.presenter_collection.should eq(Brainstem.presenter_collection(nil))
477
+ end
478
+ end
479
+
480
+ describe "for! method" do
481
+ it "raises if there is no presenter for the given class" do
482
+ lambda{ Brainstem.presenter_collection("v1").for!(String) }.should raise_error(ArgumentError)
483
+ end
484
+ end
485
+ end
486
+ end