brainstem 0.2.6.1 → 1.0.0.pre.1

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 (56) hide show
  1. checksums.yaml +5 -13
  2. data/CHANGELOG.md +16 -2
  3. data/Gemfile.lock +51 -36
  4. data/README.md +531 -110
  5. data/brainstem.gemspec +6 -2
  6. data/lib/brainstem.rb +25 -9
  7. data/lib/brainstem/concerns/controller_param_management.rb +22 -0
  8. data/lib/brainstem/concerns/error_presentation.rb +58 -0
  9. data/lib/brainstem/concerns/inheritable_configuration.rb +29 -0
  10. data/lib/brainstem/concerns/lookup.rb +30 -0
  11. data/lib/brainstem/concerns/presenter_dsl.rb +111 -0
  12. data/lib/brainstem/controller_methods.rb +17 -8
  13. data/lib/brainstem/dsl/association.rb +55 -0
  14. data/lib/brainstem/dsl/associations_block.rb +12 -0
  15. data/lib/brainstem/dsl/base_block.rb +31 -0
  16. data/lib/brainstem/dsl/conditional.rb +25 -0
  17. data/lib/brainstem/dsl/conditionals_block.rb +15 -0
  18. data/lib/brainstem/dsl/configuration.rb +112 -0
  19. data/lib/brainstem/dsl/field.rb +68 -0
  20. data/lib/brainstem/dsl/fields_block.rb +25 -0
  21. data/lib/brainstem/preloader.rb +98 -0
  22. data/lib/brainstem/presenter.rb +325 -134
  23. data/lib/brainstem/presenter_collection.rb +82 -286
  24. data/lib/brainstem/presenter_validator.rb +96 -0
  25. data/lib/brainstem/query_strategies/README.md +107 -0
  26. data/lib/brainstem/query_strategies/base_strategy.rb +62 -0
  27. data/lib/brainstem/query_strategies/filter_and_search.rb +50 -0
  28. data/lib/brainstem/query_strategies/filter_or_search.rb +103 -0
  29. data/lib/brainstem/test_helpers.rb +5 -1
  30. data/lib/brainstem/version.rb +1 -1
  31. data/spec/brainstem/concerns/controller_param_management_spec.rb +42 -0
  32. data/spec/brainstem/concerns/error_presentation_spec.rb +113 -0
  33. data/spec/brainstem/concerns/inheritable_configuration_spec.rb +210 -0
  34. data/spec/brainstem/concerns/presenter_dsl_spec.rb +412 -0
  35. data/spec/brainstem/controller_methods_spec.rb +15 -27
  36. data/spec/brainstem/dsl/association_spec.rb +123 -0
  37. data/spec/brainstem/dsl/conditional_spec.rb +93 -0
  38. data/spec/brainstem/dsl/configuration_spec.rb +1 -0
  39. data/spec/brainstem/dsl/field_spec.rb +212 -0
  40. data/spec/brainstem/preloader_spec.rb +137 -0
  41. data/spec/brainstem/presenter_collection_spec.rb +565 -244
  42. data/spec/brainstem/presenter_spec.rb +726 -167
  43. data/spec/brainstem/presenter_validator_spec.rb +209 -0
  44. data/spec/brainstem/query_strategies/filter_and_search_spec.rb +46 -0
  45. data/spec/brainstem/query_strategies/filter_or_search_spec.rb +45 -0
  46. data/spec/spec_helper.rb +11 -3
  47. data/spec/spec_helpers/db.rb +32 -65
  48. data/spec/spec_helpers/presenters.rb +124 -29
  49. data/spec/spec_helpers/rr.rb +11 -0
  50. data/spec/spec_helpers/schema.rb +115 -0
  51. metadata +126 -30
  52. data/lib/brainstem/association_field.rb +0 -53
  53. data/lib/brainstem/engine.rb +0 -4
  54. data/pkg/brainstem-0.2.5.gem +0 -0
  55. data/pkg/brainstem-0.2.6.gem +0 -0
  56. data/spec/spec_helpers/cleanup.rb +0 -23
@@ -1,30 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Brainstem::Presenter do
4
- describe "class methods" do
5
-
6
- describe "presents method" do
7
- before do
8
- @klass = Class.new(Brainstem::Presenter)
9
- end
10
-
11
- it "records itself as the presenter for the named class as a string" do
12
- @klass.presents "String"
13
- expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
14
- end
15
-
16
- it "records itself as the presenter for the given class" do
17
- @klass.presents String
18
- expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
19
- end
20
-
21
- it "records itself as the presenter for the named classes" do
22
- @klass.presents String, Array
23
- expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
24
- expect(Brainstem.presenter_collection.for(Array)).to be_a(@klass)
25
- end
26
- end
27
-
4
+ describe "class behavior" do
28
5
  describe "implicit namespacing" do
29
6
  module V1
30
7
  class SomePresenter < Brainstem::Presenter
@@ -33,7 +10,8 @@ describe Brainstem::Presenter do
33
10
 
34
11
  it "uses the closest module name as the presenter namespace" do
35
12
  V1::SomePresenter.presents String
36
- expect(Brainstem.presenter_collection(:v1).for(String)).to be_a(V1::SomePresenter)
13
+ expect(Brainstem.presenter_collection('v1').for(String)).to be_a(V1::SomePresenter)
14
+ expect(V1::SomePresenter.namespace).to eq 'v1'
37
15
  end
38
16
 
39
17
  it "does not map namespaced presenters into the default namespace" do
@@ -42,67 +20,264 @@ describe Brainstem::Presenter do
42
20
  end
43
21
  end
44
22
 
45
- describe "helper method" do
46
- before do
47
- @klass = Class.new(Brainstem::Presenter) do
48
- def call_helper
49
- foo
23
+ describe '.presents' do
24
+ let!(:presenter_class) { Class.new(Brainstem::Presenter) }
25
+
26
+ it 'records itself as the presenter for the given class' do
27
+ presenter_class.presents String
28
+ expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
29
+ end
30
+
31
+ it 'records itself as the presenter for the given classes' do
32
+ presenter_class.presents String, Array
33
+ expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
34
+ expect(Brainstem.presenter_collection.for(Array)).to be_a(presenter_class)
35
+ end
36
+
37
+ it 'can be called more than once' do
38
+ presenter_class.presents String
39
+ presenter_class.presents Array
40
+ expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
41
+ expect(Brainstem.presenter_collection.for(Array)).to be_a(presenter_class)
42
+ end
43
+
44
+ it 'returns the set of presented classes' do
45
+ expect(presenter_class.presents(String)).to eq([String])
46
+ expect(presenter_class.presents(Array)).to eq([String, Array])
47
+ expect(presenter_class.presents).to eq([String, Array])
48
+ end
49
+
50
+ it 'removes duplicates when called more then once' do
51
+ expect(presenter_class.presents(Array)).to eq([Array])
52
+ expect(presenter_class.presents(Array, Array)).to eq([Array])
53
+ end
54
+
55
+ it 'should not be inherited' do
56
+ presenter_class.presents(String)
57
+ expect(presenter_class.presents).to eq [String]
58
+ subclass = Class.new(presenter_class)
59
+ expect(subclass.presents).to eq []
60
+ subclass.presents(Array)
61
+ expect(subclass.presents).to eq [Array]
62
+ expect(presenter_class.presents).to eq [String]
63
+ end
64
+
65
+ it 'raises an error when given a string' do
66
+ expect(lambda {
67
+ presenter_class.presents 'Array'
68
+ }).to raise_error(/Brainstem Presenter#presents now expects a Class instead of a class name/)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe "#group_present" do
74
+ let(:presenter_class) do
75
+ Class.new(Brainstem::Presenter) do
76
+ presents Workspace
77
+
78
+ helper do
79
+ def helper_method(model)
80
+ Task.where(:workspace_id => model.id)[0..1]
50
81
  end
51
82
  end
52
- @helper = Module.new do
53
- def foo
54
- "I work"
55
- end
83
+
84
+ fields do
85
+ field :updated_at, :datetime
56
86
  end
57
- end
58
87
 
59
- it "includes and extends the given module" do
60
- expect { @klass.new.call_helper }.to raise_error
61
- @klass.helper @helper
62
- expect(@klass.new.call_helper).to eq("I work")
63
- expect(@klass.foo).to eq("I work")
88
+ associations do
89
+ association :tasks, Task
90
+ association :user, User
91
+ association :missing_user, User
92
+ association :something, User, via: :user
93
+ association :lead_user, User
94
+ association :lead_user_with_lambda, User, dynamic: lambda { |model| model.user }
95
+ association :tasks_with_lambda, Task, dynamic: lambda { |model| Task.where(:workspace_id => model.id) }
96
+ association :tasks_with_helper_lambda, Task, dynamic: lambda { |model| helper_method(model) }
97
+ association :tasks_with_lookup, Task, lookup: lambda { |models| Task.where(workspace_id: models.map(&:id)).group_by { |task| task.workspace_id } }
98
+ association :tasks_with_lookup_fetch, Task,
99
+ lookup: lambda { |models| Task.where(workspace_id: models.map(&:id)).group_by { |task| task.workspace_id } },
100
+ lookup_fetch: lambda { |lookup, model| lookup[model.id] }
101
+ association :synthetic, :polymorphic
102
+ end
64
103
  end
65
104
  end
105
+
106
+ let(:workspace) { Workspace.find_by_title("bob workspace 1") }
107
+ let(:presenter) { presenter_class.new }
66
108
 
67
- describe "filter method" do
68
- before do
69
- @klass = Class.new(Brainstem::Presenter)
109
+ describe "the field DSL" do
110
+ let(:presenter_class) { Class.new(WorkspacePresenter) }
111
+ let(:presenter) { presenter_class.new }
112
+ let(:model) { Workspace.find(1) }
113
+
114
+ it 'calls named methods' do
115
+ expect(presenter.group_present([model]).first['title']).to eq model.title
70
116
  end
71
117
 
72
- it "creates an entry in the filters class ivar" do
73
- @klass.filter(:foo, :default => true) { 1 }
74
- expect(@klass.filters[:foo][0]).to eq({"default" => true})
75
- expect(@klass.filters[:foo][1]).to be_a(Proc)
118
+ it 'can call methods with :via' do
119
+ presenter.configuration[:fields][:title].options[:via] = :description
120
+ expect(presenter.group_present([model]).first['title']).to eq model.description
76
121
  end
77
122
 
78
- it "accepts names without blocks" do
79
- @klass.filter(:foo)
80
- expect(@klass.filters[:foo][1]).to be_nil
123
+ it 'can call a dynamic lambda' do
124
+ expect(presenter.group_present([model]).first['dynamic_title']).to eq "title: #{model.title}"
81
125
  end
82
- end
83
126
 
84
- describe "search method" do
85
- before do
86
- @klass = Class.new(Brainstem::Presenter)
127
+ it 'can call a lookup lambda' do
128
+ expect(presenter.group_present([model]).first['lookup_title']).to eq "lookup_title: #{model.title}"
129
+ end
130
+
131
+ it 'can call a lookup_fetch lambda' do
132
+ expect(presenter.group_present([model]).first['lookup_fetch_title']).to eq "lookup_fetch_title: #{model.title}"
87
133
  end
88
134
 
89
- it "creates an entry in the search class ivar" do
90
- @klass.search do end
91
- expect(@klass.search_block).to be_a(Proc)
135
+ it 'handles nesting' do
136
+ expect(presenter.group_present([model]).first['permissions']['access_level']).to eq 2
137
+ end
138
+
139
+ describe 'handling of conditional fields' do
140
+ it 'does not return conditional fields when their :if conditionals do not match' do
141
+ expect(presenter.group_present([model]).first['secret']).to be_nil
142
+ expect(presenter.group_present([model]).first['bob_title']).to be_nil
143
+ end
144
+
145
+ it 'returns conditional fields when their :if matches' do
146
+ model.title = 'hello'
147
+ expect(presenter.group_present([model]).first['hello_title']).to eq 'title is hello'
148
+ end
149
+
150
+ it 'returns fields with the :if option only when all of the conditionals in that :if are true' do
151
+ model.title = 'hello'
152
+ presenter.class.helper do
153
+ def current_user
154
+ 'not bob'
155
+ end
156
+ end
157
+ expect(presenter.group_present([model]).first['secret']).to be_nil
158
+ presenter.class.helper do
159
+ def current_user
160
+ 'bob'
161
+ end
162
+ end
163
+ expect(presenter.group_present([model]).first['secret']).to eq model.secret_info
164
+ end
165
+
166
+ describe "caching of conditional evaluations" do
167
+ it 'only runs model conditionals once per model' do
168
+ model_id_call_count = { 1 => 0, 2 => 0 }
169
+
170
+ presenter_class.conditionals do
171
+ model :model_id_is_two, lambda { |model| model_id_call_count[model.id] += 1; model.id == 2 }
172
+ end
173
+
174
+ presenter_class.fields do
175
+ field :only_on_model_two, :string,
176
+ dynamic: lambda { "some value" },
177
+ if: :model_id_is_two
178
+ field :another_only_on_model_two, :string,
179
+ dynamic: lambda { "some value" },
180
+ if: :model_id_is_two
181
+ end
182
+
183
+ results = presenter.group_present([Workspace.find(1), Workspace.find(2)])
184
+ expect(results.first['only_on_model_two']).not_to be_present
185
+ expect(results.last['only_on_model_two']).to be_present
186
+ expect(results.first['another_only_on_model_two']).not_to be_present
187
+ expect(results.last['another_only_on_model_two']).to be_present
188
+ expect(results.first['id']).to eq '1'
189
+ expect(results.last['id']).to eq '2'
190
+
191
+ expect(model_id_call_count).to eq({ 1 => 1, 2 => 1 })
192
+ end
193
+
194
+ it 'only runs request conditionals once per request' do
195
+ call_count = 0
196
+
197
+ presenter_class.conditionals do
198
+ request :new_request_conditional, lambda { call_count += 1 }
199
+ end
200
+
201
+ presenter_class.fields do
202
+ field :new_field, :string,
203
+ dynamic: lambda { "new_field value" },
204
+ if: :new_request_conditional
205
+ field :new_field2, :string,
206
+ dynamic: lambda { "new_field2 value" },
207
+ if: :new_request_conditional
208
+ end
209
+
210
+ presenter.group_present([Workspace.find(1), Workspace.find(2)])
211
+
212
+ expect(call_count).to eq 1
213
+ end
214
+ end
215
+ end
216
+
217
+ describe 'helpers in dynamic fields' do
218
+ let(:presenter_class) do
219
+ Class.new(Brainstem::Presenter) do
220
+ helper do
221
+ def counter
222
+ @count ||= 0
223
+ @count += 1
224
+ end
225
+ end
226
+
227
+ fields do
228
+ field :memoized_helper_value1, :integer, dynamic: lambda { |model| counter }
229
+ field :memoized_helper_value2, :integer, dynamic: lambda { |model| counter }
230
+ field :memoized_helper_value3, :integer, dynamic: lambda { |model| counter }
231
+ field :memoized_helper_value4, :integer, dynamic: lambda { |model| counter }
232
+ end
233
+ end
234
+ end
235
+
236
+ let(:presenter) { presenter_class.new }
237
+
238
+ it 'shares the helper instance across fields, but not across instances' do
239
+ fields = presenter.group_present([model, model])
240
+ expect(fields[0].slice(*%w[memoized_helper_value1 memoized_helper_value2 memoized_helper_value3 memoized_helper_value4]).values).to match_array [1, 2, 3, 4]
241
+ expect(fields[1].slice(*%w[memoized_helper_value1 memoized_helper_value2 memoized_helper_value3 memoized_helper_value4]).values).to match_array [1, 2, 3, 4]
242
+ end
243
+ end
244
+
245
+ describe 'handling of optional fields' do
246
+ it 'does not include optional fields by default' do
247
+ expect(presenter.group_present([model]).first).not_to have_key('expensive_title')
248
+ end
249
+
250
+ it 'includes optional fields when explicitly requested' do
251
+ presented_workspace = presenter.group_present([model], [], optional_fields: ['expensive_title', 'expensive_title2']).first
252
+
253
+ expect(presented_workspace).to have_key('expensive_title')
254
+ expect(presented_workspace).to have_key('expensive_title2')
255
+ expect(presented_workspace).not_to have_key('expensive_title3')
256
+ end
257
+
258
+ context 'handling of conditional' do
259
+ it 'does not include field when condition is not met' do
260
+ model.title = 'Not hello'
261
+ presented_workspace = presenter.group_present([model], [], optional_fields: ['conditional_expensive_title']).first
262
+ expect(presented_workspace).not_to have_key('conditional_expensive_title')
263
+ end
264
+
265
+ it 'includes field when condition is met' do
266
+ model.title = 'hello'
267
+ presented_workspace = presenter.group_present([model], [], optional_fields: ['conditional_expensive_title']).first
268
+ expect(presented_workspace).to have_key('conditional_expensive_title')
269
+ end
270
+ end
92
271
  end
93
272
  end
94
- end
95
273
 
96
- describe "post_process hooks" do
97
274
  describe "adding object ids as strings" do
98
275
  before do
99
276
  post_presenter = Class.new(Brainstem::Presenter) do
100
277
  presents Post
101
278
 
102
- def present(model)
103
- {
104
- :body => model.body,
105
- }
279
+ fields do
280
+ field :body, :string
106
281
  end
107
282
  end
108
283
 
@@ -111,167 +286,551 @@ describe Brainstem::Presenter do
111
286
  end
112
287
 
113
288
  it "outputs the associated object's id and type" do
114
- data = @presenter.present_and_post_process(@post)
115
- expect(data[:id]).to eq(@post.id.to_s)
116
- expect(data[:body]).to eq(@post.body)
289
+ data = @presenter.group_present([@post]).first
290
+ expect(data['id']).to eq(@post.id.to_s)
291
+ expect(data['body']).to eq(@post.body)
117
292
  end
118
293
  end
119
294
 
120
295
  describe "converting dates and times" do
121
296
  it "should convert all Time-and-date-like objects to iso8601" do
122
- class TimePresenter < Brainstem::Presenter
123
- def present(model)
124
- {
125
- :time => Time.now,
126
- :date => Date.new,
127
- :recursion => {
128
- :time => Time.now,
129
- :something => [Time.now, :else],
130
- :foo => :bar
131
- }
132
- }
297
+ presenter = Class.new(Brainstem::Presenter) do
298
+ fields do
299
+ field :time, :datetime, dynamic: lambda { Time.now }
300
+ field :date, :date, dynamic: lambda { Date.new }
301
+ fields :recursion do
302
+ field :time, :datetime, dynamic: lambda { Time.now }
303
+ field :something, :datetime, dynamic: lambda { [Time.now, :else] }
304
+ field :foo, :string, dynamic: lambda { :bar }
305
+ end
133
306
  end
134
307
  end
135
308
 
136
309
  iso8601_time = /\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}[-+]\d{2}:\d{2}/
137
310
  iso8601_date = /\d{4}-\d{2}-\d{2}/
138
311
 
139
- struct = TimePresenter.new.present_and_post_process(Workspace.first)
140
- expect(struct[:time]).to match(iso8601_time)
141
- expect(struct[:date]).to match(iso8601_date)
142
- expect(struct[:recursion][:time]).to match(iso8601_time)
143
- expect(struct[:recursion][:something].first).to match(iso8601_time)
144
- expect(struct[:recursion][:something].last).to eq(:else)
145
- expect(struct[:recursion][:foo]).to eq(:bar)
312
+ struct = presenter.new.group_present([Workspace.first]).first
313
+ expect(struct['time']).to match(iso8601_time)
314
+ expect(struct['date']).to match(iso8601_date)
315
+ expect(struct['recursion']['time']).to match(iso8601_time)
316
+ expect(struct['recursion']['something'].first).to match(iso8601_time)
317
+ expect(struct['recursion']['something'].last).to eq(:else)
318
+ expect(struct['recursion']['foo']).to eq(:bar)
146
319
  end
147
320
  end
148
321
 
149
- describe "outputting polymorphic associations" do
150
- before do
151
- some_presenter = Class.new(Brainstem::Presenter) do
152
- presents Post
322
+ describe "outputting associations" do
323
+ it "should not convert or return non-included associations, but should return <association>_id for belongs_to relationships, plus all fields" do
324
+ json = presenter.group_present([workspace], []).first
325
+ expect(json.keys).to match_array %w[id updated_at something_id user_id]
326
+ end
153
327
 
154
- def present(model)
155
- {
156
- :body => model.body,
157
- :subject => association(:subject),
158
- :another_subject => association(:subject),
159
- :something_else => association(:subject, :ignore_type => true)
160
- }
161
- end
162
- end
328
+ it "should convert requested has_many associations (includes) into the <association>_ids format" do
329
+ expect(workspace.tasks.length).to be > 0
330
+ expect(presenter.group_present([workspace], ['tasks']).first['task_ids']).to match_array(workspace.tasks.map(&:id).map(&:to_s))
331
+ end
332
+
333
+ it "should ignore unknown associations" do
334
+ result = presenter.group_present(Workspace.all.to_a, ['tasks', 'unknown'])
335
+ expect(result.length).to eq Workspace.count
336
+ expect(result.last['task_ids']).to eq Workspace.last.tasks.pluck(:id).map(&:to_s)
337
+ end
338
+
339
+ it "should allow has_many associations to work on groups of models" do
340
+ result = presenter.group_present(Workspace.all.to_a, ['tasks'])
341
+ expect(result.length).to eq Workspace.count
342
+ first_workspace_tasks = Workspace.first.tasks.pluck(:id).map(&:to_s)
343
+ last_workspace_tasks = Workspace.last.tasks.pluck(:id).map(&:to_s)
344
+ expect(last_workspace_tasks.length).to eq 1
345
+ expect(result.last['task_ids']).to eq last_workspace_tasks
346
+ expect(result.first['task_ids']).to eq first_workspace_tasks
347
+ end
348
+
349
+ it "should convert requested belongs_to and has_one associations into the <association>_id format when requested" do
350
+ expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(workspace.user.id.to_s)
351
+ end
352
+
353
+ it "should convert requested belongs_to and has_one associations into the <association>_id format when requested, even if they're not found" do
354
+ expect(presenter.group_present([workspace], ['missing_user']).first).to have_key('missing_user_id')
355
+ expect(presenter.group_present([workspace], ['missing_user']).first['missing_user_id']).to eq(nil)
356
+ end
357
+
358
+ it "converts non-association models into <model>_id format when they are requested" do
359
+ expect(presenter.group_present([workspace], ['lead_user']).first['lead_user_id']).to eq(workspace.lead_user.id.to_s)
360
+ end
361
+
362
+ it "handles associations provided with lambdas" do
363
+ expect(presenter.group_present([workspace], ['lead_user_with_lambda']).first['lead_user_with_lambda_id']).to eq(workspace.lead_user.id.to_s)
364
+ expect(presenter.group_present([workspace], ['tasks_with_lambda']).first['tasks_with_lambda_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
365
+ end
366
+
367
+ it "handles associations provided with a lookup" do
368
+ expect(presenter.group_present([workspace], ['tasks_with_lookup']).first['tasks_with_lookup_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
369
+ end
370
+
371
+ it "handles associations provided with a lookup_fetch" do
372
+ expect(presenter.group_present([workspace], ['tasks_with_lookup_fetch']).first['tasks_with_lookup_fetch_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
373
+ end
163
374
 
164
- @presenter = some_presenter.new
375
+ it "handles helpers method calls in association lambdas" do
376
+ expect(presenter.group_present([workspace], ['tasks_with_helper_lambda']).first['tasks_with_helper_lambda_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s)[0..1])
165
377
  end
166
378
 
167
- let(:presented_data) { @presenter.present_and_post_process(post) }
379
+ it "should return <association>_id fields when the given association ids exist on the model whether it is requested or not" do
380
+ expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(workspace.user_id.to_s)
168
381
 
169
- context "when polymorphic association exists" do
170
- let(:post) { Post.find(1) }
382
+ json = presenter.group_present([workspace], []).first
383
+ expect(json.keys).to match_array %w[user_id something_id id updated_at]
384
+ expect(json['user_id']).to eq(workspace.user_id.to_s)
385
+ expect(json['something_id']).to eq(workspace.user_id.to_s)
386
+ end
171
387
 
388
+ it "should return null, not empty string when ids are missing" do
389
+ workspace.user = nil
390
+ workspace.tasks = []
391
+ expect(presenter.group_present([workspace], ['lead_user_with_lambda']).first['lead_user_with_lambda_id']).to eq(nil)
392
+ expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(nil)
393
+ expect(presenter.group_present([workspace], ['something']).first['something_id']).to eq(nil)
394
+ expect(presenter.group_present([workspace], ['tasks']).first['task_ids']).to eq([])
395
+ end
172
396
 
173
- it "outputs the object as a hash with the id & class table name" do
174
- expect(presented_data[:subject_ref]).to eq({ :id => post.subject.id.to_s,
175
- :key => post.subject.class.table_name })
397
+ describe "polymorphic associations" do
398
+ let(:some_presenter) do
399
+ Class.new(Brainstem::Presenter) do
400
+ presents Post
401
+
402
+ fields do
403
+ field :body, :string
404
+ end
405
+
406
+ associations do
407
+ association :subject, :polymorphic
408
+ association :another_subject, :polymorphic, via: :subject
409
+ association :forced_model, Workspace, via: :subject
410
+ association :things, :polymorphic
411
+ end
412
+ end
176
413
  end
177
414
 
178
- it "outputs custom names for the object as a hash with the id & class table name" do
179
- expect(presented_data[:another_subject_ref]).to eq({ :id => post.subject.id.to_s,
180
- :key => post.subject.class.table_name })
415
+ let(:presenter) { some_presenter.new }
416
+
417
+ context "when asking for the association" do
418
+ let(:presented_data) { presenter.group_present([post], %w[subject another_subject forced_model things]).first }
419
+
420
+ context "when polymorphic association exists" do
421
+ let(:post) { Post.find(1) }
422
+
423
+ it "outputs the object as a hash with the id & class table name" do
424
+ expect(presented_data['subject_ref']).to eq({ 'id' => post.subject.id.to_s,
425
+ 'key' => 'workspaces' })
426
+ end
427
+
428
+ it "outputs custom names for the object as a hash with the id & class table name" do
429
+ expect(presented_data['another_subject_ref']).to eq({ 'id' => post.subject.id.to_s,
430
+ 'key' => 'workspaces' })
431
+ end
432
+
433
+ context "presenting a mixture of things" do
434
+ it 'will return a *_refs array' do
435
+ expect(presented_data['thing_refs']).to eq [
436
+ { 'id' => '1', 'key' => 'workspaces' },
437
+ { 'id' => '1', 'key' => 'posts' },
438
+ { 'id' => '1', 'key' => 'tasks' }
439
+ ]
440
+ end
441
+ end
442
+
443
+ context "for STI targets" do
444
+ let(:post) { Post.create!(subject: Attachments::PostAttachment.first, user: User.first, body: '1 2 3') }
445
+
446
+ it "uses the brainstem_key from the presenter" do
447
+ expect(presented_data['subject_ref']).to eq({ 'id' => post.subject_id.to_s,
448
+ 'key' => 'attachments' })
449
+ end
450
+
451
+ it "uses the correct namespace when finding a presenter" do
452
+ module V2
453
+ class NewPostPresenter < Brainstem::Presenter
454
+ presents Post
455
+
456
+ associations do
457
+ association :subject, :polymorphic
458
+ end
459
+ end
460
+ end
461
+
462
+ expect {
463
+ V2::NewPostPresenter.new.group_present([post], %w[subject]).first
464
+ }.to raise_error(/Unable to find a presenter for class Attachments::PostAttachment/)
465
+ end
466
+ end
467
+
468
+ it "skips the polymorphic handling when a model is given" do
469
+ expect(presented_data['forced_model_id']).to eq(post.subject.id.to_s)
470
+ expect(presented_data).not_to have_key('forced_model_type')
471
+ expect(presented_data).not_to have_key('forced_model_ref')
472
+ end
473
+
474
+ describe "the legacy :always_return_ref_with_sti_base option" do
475
+ before do
476
+ some_presenter.associations do
477
+ association :always_subject, :polymorphic, via: :subject,
478
+ always_return_ref_with_sti_base: true
479
+ end
480
+ end
481
+
482
+ let(:post) { Post.create!(subject: Attachments::PostAttachment.first, user: User.first, body: '1 2 3') }
483
+
484
+ describe 'when the presenter can be found' do
485
+ before do
486
+ Class.new(Brainstem::Presenter) do
487
+ presents Attachments::Base
488
+
489
+ brainstem_key :foo
490
+ end
491
+ end
492
+
493
+ it "always returns the *_ref object, even when not included" do
494
+ expect(presented_data['always_subject_ref']).to eq({ 'id' => post.subject.id.to_s,
495
+ 'key' => 'foo' })
496
+ end
497
+ end
498
+
499
+ # It tries to find the key based on the *_type value in the DB (which will be the STI base class, and may error if no presenter exists)
500
+ describe 'when the presenter cannot be found' do
501
+ it "raises an error" do
502
+ expect { presented_data['always_subject_ref'] }.to raise_error(/Unable to find a presenter for class Attachments::Base/)
503
+ end
504
+ end
505
+ end
506
+ end
507
+
508
+ context "when polymorphic association does not exist" do
509
+ let(:post) { Post.find(3) }
510
+
511
+ it "outputs nil" do
512
+ expect(presented_data).to have_key('subject_ref')
513
+ expect(presented_data['subject_ref']).to be_nil
514
+ end
515
+
516
+ it "outputs nil" do
517
+ expect(presented_data).to have_key('another_subject_ref')
518
+ expect(presented_data['another_subject_ref']).to be_nil
519
+ end
520
+ end
181
521
  end
182
522
 
183
- it "skips the polymorphic handling when ignore_type is true" do
184
- expect(presented_data[:something_else_id]).to eq(post.subject.id.to_s)
185
- expect(presented_data).not_to have_key(:something_else_type)
186
- expect(presented_data).not_to have_key(:something_else_ref)
523
+ context "when not asking for the association" do
524
+ let(:presented_data) { presenter.group_present([post]).first }
525
+ let(:post) { Post.find(1) }
526
+
527
+ it "does not include the reference" do
528
+ expect(presented_data).to_not have_key('subject_ref')
529
+ expect(presented_data).to_not have_key('another_subject_ref')
530
+ end
187
531
  end
188
532
  end
189
533
 
190
- context "when polymorphic association does not exist" do
191
- let(:post) { Post.find(3) }
534
+ context "when the model has an <association>_id method but no column" do
535
+ it "does not include the <association>_id field" do
536
+ def workspace.synthetic_id
537
+ raise "this explodes because it's not an association"
538
+ end
539
+ expect(presenter.group_present([workspace], []).first).not_to have_key('synthetic_id')
540
+ end
541
+ end
542
+ end
192
543
 
193
- it "outputs nil" do
194
- expect(presented_data[:subject_ref]).to be_nil
544
+ describe "preloading" do
545
+
546
+ #
547
+ # We have three strategies for introspecting what the AR Preloader
548
+ # receives:
549
+ #
550
+ # 1. Actually looking at what AR receives.
551
+ #
552
+ # This is sub-optimal because AR works differently between versions
553
+ # 3 and 4, so introspecting is difficult.
554
+ #
555
+ # 2. Looking at what the proc that encapsulates the AR methods is
556
+ # called with.
557
+ #
558
+ # This is difficult to do because we don't control the instantiation
559
+ # of the Preloader, which makes injecting this hard.
560
+ #
561
+ # 3. Intercept the instantiating parent function and append the
562
+ # inspectable proc.
563
+ #
564
+ # This is probably the grossest of all the above options, and I
565
+ # suspect it's the greatest indication that we're testing
566
+ # inappropriately here -- a fact that I think bears weight given
567
+ # we're making unit-level assertions in an integration spec.
568
+ # However, there's also some value in asserting that these are
569
+ # passed through without digging into Rails internals. However,
570
+ # further 'purity' improvements could be made by introspecting on
571
+ # AR's actual data structures.
572
+ #
573
+ # That's about three shades on the side of overkill, though.
574
+ #
575
+ def preloader_should_receive(hsh)
576
+ preload_method = Object.new
577
+ mock(preload_method).call(anything, anything) do |models, args|
578
+ expect(args).to eq(hsh)
195
579
  end
196
580
 
197
- it "outputs nil" do
198
- expect(presented_data[:another_subject_ref]).to be_nil
581
+ stub(Brainstem::Preloader).preload(anything, anything, anything) do |*args|
582
+ args << preload_method
583
+ Brainstem::Preloader.new(*args).call
199
584
  end
200
585
  end
586
+
587
+
588
+ it "preloads associations when they are full model-level associations" do
589
+ preloader_should_receive("tasks" => [], "user" => [])
590
+ presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
591
+ end
592
+
593
+ it "includes any associations declared via the preload DSL directive" do
594
+ preloader_should_receive("tasks" => [], "posts" => [])
595
+ presenter_class.preload :posts
596
+
597
+ presenter.group_present(Workspace.order('id desc'), %w[tasks lead_user tasks_with_lambda])
598
+ end
599
+
600
+ it "includes any string associations declared via the preload DSL directive" do
601
+ preloader_should_receive("tasks" => [], "user" => [])
602
+ presenter_class.preload 'user'
603
+
604
+ presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
605
+ end
606
+
607
+ it "includes any nested hash associations declared via the preload DSL directive" do
608
+ preloader_should_receive("tasks" => [], "user" => [:workspaces], "posts" => ["subject", "user"])
609
+ presenter_class.preload :tasks, "user", "unknown", { "posts" => "subject", "foo" => "bar" },{ :user => :workspaces, "posts" => "user" }
610
+
611
+ presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
612
+ end
201
613
  end
614
+ end
202
615
 
203
- describe "outputting associations" do
204
- before do
205
- some_presenter = Class.new(Brainstem::Presenter) do
206
- presents Workspace
207
-
208
- def present(model)
209
- {
210
- :updated_at => model.updated_at,
211
- :tasks => association(:tasks),
212
- :user => association(:user),
213
- :something => association(:user),
214
- :lead_user => association(:lead_user),
215
- :lead_user_with_lambda => association(:json_name => "users") { |model| model.user },
216
- :tasks_with_lambda => association(:json_name => "tasks") { |model| Task.where(:workspace_id => model) },
217
- :synthetic => association(:synthetic)
218
- }
616
+ describe "#extract_filters" do
617
+ let(:presenter_class) { WorkspacePresenter }
618
+ let(:presenter) { presenter_class.new }
619
+
620
+ it 'returns only known filters' do
621
+ presenter_class.filter :owned_by
622
+ presenter_class.filter(:bar) { |scope| scope }
623
+ expect(presenter.extract_filters({ 'foo' => 'hi' })).to eq({})
624
+ expect(presenter.extract_filters({ 'owned_by' => '2' })).to eq({ 'owned_by' => '2' })
625
+ expect(presenter.extract_filters({ 'owned_by' => [2] })).to eq({ 'owned_by' => [2] })
626
+ expect(presenter.extract_filters({ 'owned_by' => { :ids => [2], 2 => [1] }})).to eq({ 'owned_by' => { :ids => [2], 2 => [1] }})
627
+ end
628
+
629
+ it "converts 'true' and 'false' into true and false" do
630
+ presenter_class.filter :owned_by
631
+ expect(presenter.extract_filters({ 'owned_by' => 'true' })).to eq({ 'owned_by' => true })
632
+ expect(presenter.extract_filters({ 'owned_by' => 'TRUE' })).to eq({ 'owned_by' => true })
633
+ expect(presenter.extract_filters({ 'owned_by' => 'false' })).to eq({ 'owned_by' => false })
634
+ expect(presenter.extract_filters({ 'owned_by' => 'FALSE' })).to eq({ 'owned_by' => false })
635
+ expect(presenter.extract_filters({ 'owned_by' => 'hi' })).to eq({ 'owned_by' => 'hi' })
636
+ end
637
+
638
+ it 'defaults to applying default filters' do
639
+ presenter_class.filter :owned_by, default: '2'
640
+ expect(presenter.extract_filters({ 'owned_by' => '3' })).to eq({ 'owned_by' => '3' })
641
+ expect(presenter.extract_filters({})).to eq({ 'owned_by' => '2' })
642
+ end
643
+
644
+ it 'will skip default filters when asked' do
645
+ presenter_class.filter :owned_by, default: '2'
646
+ expect(presenter.extract_filters({ 'owned_by' => '3' }, apply_default_filters: false)).to eq({ 'owned_by' => '3' })
647
+ expect(presenter.extract_filters({}, apply_default_filters: false)).to eq({})
648
+ end
649
+
650
+ it 'ignores nil and blank values' do
651
+ presenter_class.filter :owned_by
652
+ expect(presenter.extract_filters({ 'owned_by' => nil })).to eq({})
653
+ expect(presenter.extract_filters({ 'owned_by' => '' })).to eq({})
654
+ end
655
+ end
656
+
657
+ describe "#apply_filters_to_scope" do
658
+ let(:presenter_class) { WorkspacePresenter }
659
+ let(:presenter) { presenter_class.new }
660
+ let(:scope) { Workspace.where(nil) }
661
+ let(:params) { { 'bar' => 'foo' } }
662
+ let(:options) { { apply_default_filters: true } }
663
+
664
+ before do
665
+ presenter_class.filter :owned_by, default: '2'
666
+ presenter_class.filter(:bar) { |scope| scope.where(id: 6) }
667
+ mock(presenter).extract_filters(params, options) { { 'bar' => 'foo', 'owned_by' => '2' } }
668
+ end
669
+
670
+ it 'extracts valid filters from the params' do
671
+ presenter.apply_filters_to_scope(scope, params, options)
672
+ end
673
+
674
+ it 'runs lambdas in the scope of the helper instance' do
675
+ expect(presenter.apply_filters_to_scope(scope, params, options).to_sql).to match(/id.\s*=\s*6/)
676
+ end
677
+
678
+ it 'sends symbols to the scope' do
679
+ expect(presenter.apply_filters_to_scope(scope, params, options).to_sql).to match(/id.\s*=\s*2/)
680
+ end
681
+ end
682
+
683
+ describe '#apply_ordering_to_scope' do
684
+ let(:presenter_class) { WorkspacePresenter }
685
+ let(:presenter) { presenter_class.new }
686
+ let(:scope) { Workspace.where(nil) }
687
+
688
+ it 'uses #calculate_sort_name_and_direction to extract a sort name and direction from user params' do
689
+ presenter_class.sort_order :title, 'workspaces.title'
690
+ mock(presenter).calculate_sort_name_and_direction('order' => 'title:desc') { ['title', 'desc'] }
691
+ presenter.apply_ordering_to_scope(scope, 'order' => 'title:desc')
692
+ end
693
+
694
+ context 'when the sort is a proc' do
695
+ it 'runs procs in the context of any helpers' do
696
+ presenter_class.helper do
697
+ def some_method
219
698
  end
220
699
  end
221
700
 
222
- @presenter = some_presenter.new
223
- @workspace = Workspace.find_by_title "bob workspace 1"
701
+ direction = nil
702
+ presenter_class.sort_order(:title) do |scope, d|
703
+ some_method
704
+ direction = d
705
+ scope
706
+ end
707
+
708
+ presenter.apply_ordering_to_scope(scope, 'order' => 'title:asc')
709
+ expect(direction).to eq 'asc'
224
710
  end
225
711
 
226
- it "should not convert or return non-included associations, but should return <association>_id for belongs_to relationships, plus all fields" do
227
- json = @presenter.present_and_post_process(@workspace, [])
228
- expect(json.keys).to match_array([:id, :updated_at, :something_id, :user_id])
712
+ it 'can chain multiple sorts together' do
713
+ presenter_class.sort_order(:title) do |scope|
714
+ scope.order('workspaces.title desc').order('workspaces.id desc')
715
+ end
716
+
717
+ sql = presenter.apply_ordering_to_scope(scope, 'order' => 'title').to_sql
718
+ expect(sql).to match(/order by workspaces.title desc, workspaces.id desc, "workspaces"."id" ASC/i)
719
+ # this should be ok, since the first id sort will never have a tie
229
720
  end
230
721
 
231
- it "should convert requested has_many associations (includes) into the <association>_ids format" do
232
- expect(@workspace.tasks.length).to be > 0
233
- expect(@presenter.present_and_post_process(@workspace, ["tasks"])[:task_ids]).to match_array(@workspace.tasks.map(&:id).map(&:to_s))
722
+ it 'chains the primary key onto the end' do
723
+ presenter_class.sort_order(:title) do |scope|
724
+ scope.order('workspaces.title desc')
725
+ end
726
+
727
+ sql = presenter.apply_ordering_to_scope(scope, 'order' => 'title').to_sql
728
+ expect(sql).to match(/order by workspaces.title desc, "workspaces"."id" ASC/i)
234
729
  end
730
+ end
235
731
 
236
- it "should convert requested belongs_to and has_one associations into the <association>_id format when requested" do
237
- expect(@presenter.present_and_post_process(@workspace, ["user"])[:user_id]).to eq(@workspace.user.id.to_s)
732
+ context 'when the sort is not a proc' do
733
+ before do
734
+ presenter_class.sort_order :title, 'workspaces.title'
735
+ presenter_class.sort_order :id, 'workspaces.id'
238
736
  end
239
737
 
240
- it "converts non-association models into <model>_id format when they are requested" do
241
- expect(@presenter.present_and_post_process(@workspace, ["lead_user"])[:lead_user_id]).to eq(@workspace.lead_user.id.to_s)
738
+ context 'and is a string' do
739
+ let(:order) { { 'order' => 'title:asc' } }
740
+
741
+ it 'applies the named ordering in the given direction and adds the primary key as a fallback sort' do
742
+ sql = presenter.apply_ordering_to_scope(scope, order).to_sql
743
+ expect(sql).to match(/ORDER BY workspaces.title asc, "workspaces"."id" ASC/i)
744
+ end
242
745
  end
243
746
 
244
- it "handles associations provided with lambdas" do
245
- expect(@presenter.present_and_post_process(@workspace, ["lead_user_with_lambda"])[:lead_user_with_lambda_id]).to eq(@workspace.lead_user.id.to_s)
246
- expect(@presenter.present_and_post_process(@workspace, ["tasks_with_lambda"])[:tasks_with_lambda_ids]).to eq(@workspace.tasks.map(&:id).map(&:to_s))
747
+ context 'and is a symbol' do
748
+ let(:order) { { 'order' => :title } }
749
+
750
+ it 'applies the named ordering in the given direction and adds the primary key as a fallback sort' do
751
+ sql = presenter.apply_ordering_to_scope(scope, order).to_sql
752
+ expect(sql).to match(/order by workspaces.title asc, "workspaces"."id" ASC/i)
753
+ end
247
754
  end
755
+ end
248
756
 
249
- it "should return <association>_id fields when the given association ids exist on the model whether it is requested or not" do
250
- expect(@presenter.present_and_post_process(@workspace, ["user"])[:user_id]).to eq(@workspace.user_id.to_s)
757
+ context 'when the sort is not present' do
758
+ let(:order) { '' }
251
759
 
252
- json = @presenter.present_and_post_process(@workspace, [])
253
- expect(json.keys).to match_array([:user_id, :something_id, :id, :updated_at])
254
- expect(json[:user_id]).to eq(@workspace.user_id.to_s)
255
- expect(json[:something_id]).to eq(@workspace.user_id.to_s)
760
+ it 'orders by the primary key' do
761
+ sql = presenter.apply_ordering_to_scope(scope, order).to_sql
762
+ expect(sql).to match(/order by "workspaces"."id" ASC/i)
256
763
  end
764
+ end
257
765
 
258
- it "should return null, not empty string when ids are missing" do
259
- @workspace.user = nil
260
- @workspace.tasks = []
261
- expect(@presenter.present_and_post_process(@workspace, ["lead_user_with_lambda"])[:lead_user_with_lambda_id]).to eq(nil)
262
- expect(@presenter.present_and_post_process(@workspace, ["user"])[:user_id]).to eq(nil)
263
- expect(@presenter.present_and_post_process(@workspace, ["something"])[:something_id]).to eq(nil)
264
- expect(@presenter.present_and_post_process(@workspace, ["tasks"])[:task_ids]).to eq([])
766
+ context 'when the table has no primary key' do
767
+ let(:scope) { Cthulhu.where(nil) }
768
+ let(:order) { { 'order' => 'updated_at:asc' } }
769
+
770
+ before do
771
+ class Cthulhu < Workspace
772
+ self.primary_key = nil
773
+ end
774
+
775
+ presenter_class.sort_order :updated_at, 'workspaces.updated_at'
265
776
  end
266
777
 
267
- context "when the model has an <association>_id method but no column" do
268
- it "does not include the <association>_id field" do
269
- def @workspace.synthetic_id
270
- raise "this explodes because it's not an association"
271
- end
272
- expect(@presenter.present_and_post_process(@workspace, [])).not_to have_key(:synthetic_id)
778
+ it 'does not add a fallback deterministic sort, and you deserve whatever fate befalls you' do
779
+ sql = presenter.apply_ordering_to_scope(scope, order).to_sql.squish
780
+ expect(sql).to eq("SELECT \"workspaces\".* FROM \"workspaces\" WHERE \"workspaces\".\"type\" IN ('Cthulhu') ORDER BY workspaces.updated_at asc")
781
+ end
782
+ end
783
+ end
784
+
785
+ describe "#calculate_sort_name_and_direction" do
786
+ let(:presenter_class) { WorkspacePresenter }
787
+ let(:presenter) { presenter_class.new }
788
+
789
+ it 'uses default_sort_order by default when present' do
790
+ presenter_class.default_sort_order 'foo:asc'
791
+ expect(presenter.calculate_sort_name_and_direction).to eq ['foo', 'asc']
792
+ end
793
+
794
+ it 'uses updated_at:desc when no default has been set' do
795
+ expect(presenter.calculate_sort_name_and_direction).to eq ['updated_at', 'desc']
796
+ end
797
+
798
+ it 'ignores unknown sorts' do
799
+ presenter_class.sort_order :foo, 'workspaces.foo'
800
+ expect(presenter.calculate_sort_name_and_direction('order' => 'hello:desc')).to eq ['updated_at', 'desc']
801
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:desc')).to eq ['foo', 'desc']
802
+ end
803
+
804
+ it 'sanitizes the direction' do
805
+ presenter_class.sort_order :foo, 'workspaces.foo'
806
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:drop table')).to eq ['foo', 'asc']
807
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:')).to eq ['foo', 'asc']
808
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:hi')).to eq ['foo', 'asc']
809
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:DESCE')).to eq ['foo', 'asc']
810
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:asc')).to eq ['foo', 'asc']
811
+ expect(presenter.calculate_sort_name_and_direction('order' => 'foo:;;;droptable::;;')).to eq ['foo', 'asc']
812
+ end
813
+ end
814
+
815
+ describe '#allowed_associations' do
816
+ let(:presenter_class) do
817
+ Class.new(Brainstem::Presenter) do
818
+ associations do
819
+ association :user, User
820
+ association :workspace, Workspace
821
+ association :task, Task, restrict_to_only: true
273
822
  end
274
823
  end
275
824
  end
825
+
826
+ let(:presenter_instance) { presenter_class.new }
827
+
828
+ it 'returns all associations that are not restrict_to_only' do
829
+ expect(presenter_instance.allowed_associations(is_only_query = false).keys).to match_array %w[user workspace]
830
+ end
831
+
832
+ it 'returns associations that are restrict_to_only if is_only_query is true' do
833
+ expect(presenter_instance.allowed_associations(is_only_query = true).keys).to match_array %w[user workspace task]
834
+ end
276
835
  end
277
836
  end