brainstem 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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