brainstem 1.1.1 → 1.3.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -4
  3. data/Gemfile.lock +9 -9
  4. data/README.md +134 -37
  5. data/brainstem.gemspec +1 -1
  6. data/lib/brainstem/api_docs/endpoint.rb +40 -18
  7. data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +27 -22
  8. data/lib/brainstem/api_docs/formatters/markdown/helper.rb +9 -0
  9. data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +14 -6
  10. data/lib/brainstem/api_docs/presenter.rb +3 -7
  11. data/lib/brainstem/concerns/controller_dsl.rb +138 -14
  12. data/lib/brainstem/concerns/presenter_dsl.rb +39 -6
  13. data/lib/brainstem/dsl/array_block_field.rb +25 -0
  14. data/lib/brainstem/dsl/block_field.rb +69 -0
  15. data/lib/brainstem/dsl/configuration.rb +13 -5
  16. data/lib/brainstem/dsl/field.rb +15 -1
  17. data/lib/brainstem/dsl/fields_block.rb +20 -2
  18. data/lib/brainstem/dsl/hash_block_field.rb +30 -0
  19. data/lib/brainstem/presenter.rb +10 -6
  20. data/lib/brainstem/presenter_validator.rb +20 -11
  21. data/lib/brainstem/version.rb +1 -1
  22. data/spec/brainstem/api_docs/endpoint_spec.rb +347 -14
  23. data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +106 -13
  24. data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +19 -0
  25. data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +150 -37
  26. data/spec/brainstem/api_docs/presenter_spec.rb +85 -18
  27. data/spec/brainstem/concerns/controller_dsl_spec.rb +615 -31
  28. data/spec/brainstem/concerns/inheritable_configuration_spec.rb +32 -9
  29. data/spec/brainstem/concerns/presenter_dsl_spec.rb +99 -25
  30. data/spec/brainstem/dsl/array_block_field_spec.rb +43 -0
  31. data/spec/brainstem/dsl/block_field_spec.rb +188 -0
  32. data/spec/brainstem/dsl/field_spec.rb +86 -20
  33. data/spec/brainstem/dsl/hash_block_field_spec.rb +166 -0
  34. data/spec/brainstem/presenter_collection_spec.rb +24 -24
  35. data/spec/brainstem/presenter_spec.rb +233 -9
  36. data/spec/brainstem/query_strategies/filter_and_search_spec.rb +1 -1
  37. data/spec/spec_helpers/presenters.rb +8 -0
  38. data/spec/spec_helpers/schema.rb +13 -0
  39. metadata +15 -6
@@ -69,7 +69,6 @@ describe Brainstem::Presenter do
69
69
  end
70
70
  end
71
71
 
72
-
73
72
  describe ".possible_brainstem_keys" do
74
73
  let(:presented_class) { Class.new }
75
74
  let(:other_presented_class) { Class.new }
@@ -301,6 +300,231 @@ describe Brainstem::Presenter do
301
300
  end
302
301
  end
303
302
  end
303
+
304
+ describe 'handling nested hash block fields' do
305
+ let(:presenter_class) do
306
+ Class.new(Brainstem::Presenter) do
307
+ presents Workspace
308
+
309
+ helper do
310
+ def current_user
311
+ 'jane'
312
+ end
313
+ end
314
+
315
+ conditionals do
316
+ request :user_is_bob, lambda { current_user == 'bob' }, info: 'visible only to bob'
317
+ end
318
+
319
+ fields do
320
+ fields :participant, :hash, via: :lead_user, if: :user_is_bob do
321
+ field :username, :string
322
+ end
323
+
324
+ fields :lead_user do
325
+ field :username, :string, dynamic: lambda { |workspace| workspace.lead_user.username }
326
+ end
327
+ end
328
+ end
329
+ end
330
+ let(:presenter) { presenter_class.new }
331
+
332
+ before do
333
+ stub.any_instance_of(Brainstem::DSL::Field).presentable?(model, anything) { presentable }
334
+ end
335
+
336
+ context 'when field is presentable' do
337
+ let(:presentable) { true }
338
+
339
+ it 'includes the executable hash block field' do
340
+ presented_workspace = presenter.group_present([model], []).first
341
+
342
+ expect(presented_workspace.keys).to include('participant')
343
+ end
344
+
345
+ it 'includes the non executable hash block field' do
346
+ presented_workspace = presenter.group_present([model], []).first
347
+
348
+ expect(presented_workspace.keys).to include('lead_user')
349
+ end
350
+ end
351
+
352
+ context 'when field is not presentable' do
353
+ let(:presentable) { false }
354
+
355
+ it 'does not include executable hash block fields' do
356
+ presented_workspace = presenter.group_present([model], []).first
357
+
358
+ expect(presented_workspace.keys).to_not include('participant')
359
+ end
360
+
361
+ it 'always includes non-executable hash block fields' do
362
+ presented_workspace = presenter.group_present([model], []).first
363
+
364
+ expect(presented_workspace.keys).to include('lead_user')
365
+ end
366
+ end
367
+ end
368
+
369
+ describe 'handling of nested array fields' do
370
+ let(:array_values) { [OpenStruct.new(name: 1), OpenStruct.new(name: 2)] }
371
+ let(:presenter_class) do
372
+ Class.new(Brainstem::Presenter) do
373
+ presents Workspace
374
+
375
+ fields do
376
+ fields :participants, :array, via: :members do
377
+ field :username, :string
378
+ end
379
+
380
+ fields :accessible_by, :array, dynamic: -> { [OpenStruct.new(name: 1), OpenStruct.new(name: 2)] } do
381
+ field :name, :string
382
+ field :full_name, :string, via: :name
383
+ field :formatted_name, :string, dynamic: -> (model) { model.name * 2 }
384
+ end
385
+ end
386
+ end
387
+ end
388
+ let(:presenter) { presenter_class.new }
389
+
390
+ context 'when dynamic option is specified' do
391
+ def extract_values(fields, field_name, nested_field_name)
392
+ fields[field_name].map { |data| data[nested_field_name] }
393
+ end
394
+
395
+ it 'calls named methods' do
396
+ fields = presenter.group_present([model]).first
397
+
398
+ expect(fields).to have_key('accessible_by')
399
+ expect(extract_values(fields, 'accessible_by', 'name')).to eq([1, 2])
400
+ end
401
+
402
+ it 'can call methods with :via' do
403
+ fields = presenter.group_present([model]).first
404
+
405
+ expect(fields).to have_key('accessible_by')
406
+ expect(extract_values(fields, 'accessible_by', 'full_name')).to eq([1, 2])
407
+ end
408
+
409
+ it 'can call a dynamic lambda' do
410
+ fields = presenter.group_present([model]).first
411
+
412
+ expect(fields).to have_key('accessible_by')
413
+ expect(extract_values(fields, 'accessible_by', 'formatted_name')).to eq([2, 4])
414
+ end
415
+ end
416
+
417
+ context 'when via option is specified' do
418
+ let(:participants) { [model.user] }
419
+ let(:presented_field_data) { participants.map { |user| { 'username' => user.username } } }
420
+
421
+ it 'returns an array of hashes with the specified field' do
422
+ fields = presenter.group_present([model]).first
423
+
424
+ expect(fields).to have_key('participants')
425
+ expect(fields['participants']).to eq(presented_field_data)
426
+ end
427
+ end
428
+
429
+ describe 'when nested fields specify not using the parent value' do
430
+ let(:presenter_class) do
431
+ Class.new(Brainstem::Presenter) do
432
+ presents Workspace
433
+
434
+ fields do
435
+ fields :tasks, :array, via: :tasks do
436
+ field :name, :string
437
+ field :secret, :string,
438
+ info: 'a secret, via secret_info',
439
+ via: :secret_info,
440
+ use_parent_value: false
441
+ end
442
+ end
443
+ end
444
+ end
445
+ let(:tasks) { Task.where(workspace_id: model.id).order(:id).to_a }
446
+ let(:presented_field_data) { tasks.map { |task| { 'name' => task.name, 'secret' => model.secret_info } } }
447
+
448
+ it 'returns an array of hashes while using the correct model to evaluate the properties' do
449
+ fields = presenter.group_present([model]).first
450
+
451
+ expect(fields).to have_key('tasks')
452
+ expect(fields['tasks']).to eq(presented_field_data)
453
+ end
454
+ end
455
+
456
+ describe 'handling of conditional fields' do
457
+ let(:presenter_class) do
458
+ Class.new(Brainstem::Presenter) do
459
+ presents Workspace
460
+
461
+ helper do
462
+ def current_user
463
+ 'jane'
464
+ end
465
+ end
466
+
467
+ conditionals do
468
+ model :title_is_hello, lambda { |model| model.title == 'hello' }, info: 'visible when the title is hello'
469
+ request :user_is_bob, lambda { current_user == 'bob' }, info: 'visible only to bob'
470
+ end
471
+
472
+ fields do
473
+ with_options if: :user_is_bob do
474
+ fields :tasks, :array, dynamic: lambda { |workspace| workspace.tasks.to_a } do
475
+ field :name, :string
476
+ end
477
+ end
478
+
479
+ fields :members, :array, via: :members, if: :title_is_hello do
480
+ field :hello_title, :string,
481
+ info: 'the title, when hello',
482
+ dynamic: lambda { 'title is hello' }
483
+
484
+ field :foo, :string,
485
+ info: 'a secret, via secret_info',
486
+ dynamic: lambda { 'foo' },
487
+ if: [:user_is_bob]
488
+ end
489
+ end
490
+ end
491
+ end
492
+
493
+ it 'does not return conditional fields when their :if conditionals do not match' do
494
+ fields = presenter.group_present([model]).first
495
+
496
+ expect(fields).to_not have_key('tasks')
497
+ expect(fields).to_not have_key('members')
498
+ end
499
+
500
+ it 'returns conditional fields when their :if matches' do
501
+ model.title = 'hello'
502
+
503
+ fields = presenter.group_present([model]).first
504
+
505
+ expect(fields).to_not have_key('tasks')
506
+ expect(fields).to have_key('members')
507
+ expect(fields['members'][0]).to_not have_key('foo')
508
+ expect(fields['members'][0]['hello_title']).to eq 'title is hello'
509
+ end
510
+
511
+ it 'returns fields with the :if option only when all of the conditionals in that :if are true' do
512
+ model.title = 'hello'
513
+ presenter.class.helper do
514
+ def current_user
515
+ 'bob'
516
+ end
517
+ end
518
+
519
+ fields = presenter.group_present([model]).first
520
+
521
+ expect(fields).to have_key('tasks')
522
+ expect(fields).to have_key('members')
523
+ expect(fields['members'][0]['foo']).to eq('foo')
524
+ expect(fields['members'][0]['hello_title']).to eq('title is hello')
525
+ end
526
+ end
527
+ end
304
528
  end
305
529
 
306
530
  describe "adding object ids as strings" do
@@ -650,8 +874,8 @@ describe Brainstem::Presenter do
650
874
  let(:presenter) { presenter_class.new }
651
875
 
652
876
  it 'returns only known filters' do
653
- presenter_class.filter :owned_by
654
- presenter_class.filter(:bar) { |scope| scope }
877
+ presenter_class.filter :owned_by, :integer
878
+ presenter_class.filter(:bar, :string) { |scope| scope }
655
879
  expect(presenter.extract_filters({ 'foo' => 'hi' })).to eq({})
656
880
  expect(presenter.extract_filters({ 'owned_by' => '2' })).to eq({ 'owned_by' => '2' })
657
881
  expect(presenter.extract_filters({ 'owned_by' => [2] })).to eq({ 'owned_by' => [2] })
@@ -659,7 +883,7 @@ describe Brainstem::Presenter do
659
883
  end
660
884
 
661
885
  it "converts 'true' and 'false' into true and false" do
662
- presenter_class.filter :owned_by
886
+ presenter_class.filter :owned_by, :boolean
663
887
  expect(presenter.extract_filters({ 'owned_by' => 'true' })).to eq({ 'owned_by' => true })
664
888
  expect(presenter.extract_filters({ 'owned_by' => 'TRUE' })).to eq({ 'owned_by' => true })
665
889
  expect(presenter.extract_filters({ 'owned_by' => 'false' })).to eq({ 'owned_by' => false })
@@ -668,19 +892,19 @@ describe Brainstem::Presenter do
668
892
  end
669
893
 
670
894
  it 'defaults to applying default filters' do
671
- presenter_class.filter :owned_by, default: '2'
895
+ presenter_class.filter :owned_by, :integer, default: '2'
672
896
  expect(presenter.extract_filters({ 'owned_by' => '3' })).to eq({ 'owned_by' => '3' })
673
897
  expect(presenter.extract_filters({})).to eq({ 'owned_by' => '2' })
674
898
  end
675
899
 
676
900
  it 'will skip default filters when asked' do
677
- presenter_class.filter :owned_by, default: '2'
901
+ presenter_class.filter :owned_by, :integer, default: '2'
678
902
  expect(presenter.extract_filters({ 'owned_by' => '3' }, apply_default_filters: false)).to eq({ 'owned_by' => '3' })
679
903
  expect(presenter.extract_filters({}, apply_default_filters: false)).to eq({})
680
904
  end
681
905
 
682
906
  it 'ignores nil and blank values' do
683
- presenter_class.filter :owned_by
907
+ presenter_class.filter :owned_by, :integer
684
908
  expect(presenter.extract_filters({ 'owned_by' => nil })).to eq({})
685
909
  expect(presenter.extract_filters({ 'owned_by' => '' })).to eq({})
686
910
  end
@@ -694,8 +918,8 @@ describe Brainstem::Presenter do
694
918
  let(:options) { { apply_default_filters: true } }
695
919
 
696
920
  before do
697
- presenter_class.filter :owned_by, default: '2'
698
- presenter_class.filter(:bar) { |scope| scope.where(id: 6) }
921
+ presenter_class.filter :owned_by, :integer, default: '2'
922
+ presenter_class.filter(:bar, :string) { |scope| scope.where(id: 6) }
699
923
  mock(presenter).extract_filters(params, options) { { 'bar' => 'foo', 'owned_by' => '2' } }
700
924
  end
701
925
 
@@ -35,7 +35,7 @@ describe Brainstem::QueryStrategies::FilterAndSearch do
35
35
  [search_results, search_results.count]
36
36
  end
37
37
 
38
- CheesePresenter.filter(:owned_by) { |scope, user_id| scope.owned_by(user_id.to_i) }
38
+ CheesePresenter.filter(:owned_by, :integer) { |scope, user_id| scope.owned_by(user_id.to_i) }
39
39
  CheesePresenter.sort_order(:id) { |scope, direction| scope.order("cheeses.id #{direction}") }
40
40
  end
41
41
 
@@ -32,6 +32,14 @@
32
32
  field :access_level, :integer, dynamic: lambda { 2 }
33
33
  end
34
34
 
35
+ fields :members, :array, via: :members do
36
+ field :username, :string
37
+ field :secret, :string,
38
+ info: 'a secret, via secret_info',
39
+ via: :secret_info,
40
+ use_parent_value: false
41
+ end
42
+
35
43
  field :hello_title, :string,
36
44
  info: 'the title, when hello',
37
45
  dynamic: lambda { 'title is hello' },
@@ -51,10 +51,15 @@ end
51
51
 
52
52
  class User < ActiveRecord::Base
53
53
  has_many :workspaces
54
+
55
+ def type
56
+ self.class.name
57
+ end
54
58
  end
55
59
 
56
60
  class Task < ActiveRecord::Base
57
61
  belongs_to :workspace
62
+ belongs_to :parent, :class_name => "Task"
58
63
  has_many :sub_tasks, :foreign_key => :parent_id, :class_name => "Task"
59
64
  has_many :posts
60
65
 
@@ -75,6 +80,10 @@ class Workspace < ActiveRecord::Base
75
80
  "this is secret!"
76
81
  end
77
82
 
83
+ def members
84
+ [user]
85
+ end
86
+
78
87
  def lead_user
79
88
  user
80
89
  end
@@ -82,6 +91,10 @@ class Workspace < ActiveRecord::Base
82
91
  def missing_user
83
92
  nil
84
93
  end
94
+
95
+ def type
96
+ self.class.name
97
+ end
85
98
  end
86
99
 
87
100
  class Group < Workspace
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.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mavenlink
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-15 00:00:00.000000000 Z
11
+ date: 2018-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -112,16 +112,16 @@ dependencies:
112
112
  name: mysql2
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: 0.4.10
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 0.4.10
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: database_cleaner
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -252,14 +252,17 @@ files:
252
252
  - lib/brainstem/concerns/optional.rb
253
253
  - lib/brainstem/concerns/presenter_dsl.rb
254
254
  - lib/brainstem/controller_methods.rb
255
+ - lib/brainstem/dsl/array_block_field.rb
255
256
  - lib/brainstem/dsl/association.rb
256
257
  - lib/brainstem/dsl/associations_block.rb
257
258
  - lib/brainstem/dsl/base_block.rb
259
+ - lib/brainstem/dsl/block_field.rb
258
260
  - lib/brainstem/dsl/conditional.rb
259
261
  - lib/brainstem/dsl/conditionals_block.rb
260
262
  - lib/brainstem/dsl/configuration.rb
261
263
  - lib/brainstem/dsl/field.rb
262
264
  - lib/brainstem/dsl/fields_block.rb
265
+ - lib/brainstem/dsl/hash_block_field.rb
263
266
  - lib/brainstem/help_text.txt
264
267
  - lib/brainstem/preloader.rb
265
268
  - lib/brainstem/presenter.rb
@@ -306,10 +309,13 @@ files:
306
309
  - spec/brainstem/concerns/optional_spec.rb
307
310
  - spec/brainstem/concerns/presenter_dsl_spec.rb
308
311
  - spec/brainstem/controller_methods_spec.rb
312
+ - spec/brainstem/dsl/array_block_field_spec.rb
309
313
  - spec/brainstem/dsl/association_spec.rb
314
+ - spec/brainstem/dsl/block_field_spec.rb
310
315
  - spec/brainstem/dsl/conditional_spec.rb
311
316
  - spec/brainstem/dsl/configuration_spec.rb
312
317
  - spec/brainstem/dsl/field_spec.rb
318
+ - spec/brainstem/dsl/hash_block_field_spec.rb
313
319
  - spec/brainstem/preloader_spec.rb
314
320
  - spec/brainstem/presenter_collection_spec.rb
315
321
  - spec/brainstem/presenter_spec.rb
@@ -384,10 +390,13 @@ test_files:
384
390
  - spec/brainstem/concerns/optional_spec.rb
385
391
  - spec/brainstem/concerns/presenter_dsl_spec.rb
386
392
  - spec/brainstem/controller_methods_spec.rb
393
+ - spec/brainstem/dsl/array_block_field_spec.rb
387
394
  - spec/brainstem/dsl/association_spec.rb
395
+ - spec/brainstem/dsl/block_field_spec.rb
388
396
  - spec/brainstem/dsl/conditional_spec.rb
389
397
  - spec/brainstem/dsl/configuration_spec.rb
390
398
  - spec/brainstem/dsl/field_spec.rb
399
+ - spec/brainstem/dsl/hash_block_field_spec.rb
391
400
  - spec/brainstem/preloader_spec.rb
392
401
  - spec/brainstem/presenter_collection_spec.rb
393
402
  - spec/brainstem/presenter_spec.rb