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
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'brainstem/concerns/controller_param_management'
3
+
4
+ describe Brainstem::Concerns::ControllerParamManagement do
5
+ class TasksController
6
+ include Brainstem::Concerns::ControllerParamManagement
7
+
8
+ def controller_name
9
+ 'tasks'
10
+ end
11
+ end
12
+
13
+ before do
14
+ TasksController.brainstem_model_name = nil
15
+ TasksController.brainstem_plural_model_name = nil
16
+ end
17
+
18
+ describe '.brainstem_model_name' do
19
+ it 'is settable on the controller' do
20
+ TasksController.brainstem_model_name = 'thingy'
21
+ expect(TasksController.new.brainstem_model_name).to eq 'thingy'
22
+ end
23
+
24
+ it 'has good defaults' do
25
+ expect(TasksController.new.brainstem_model_name).to eq 'task'
26
+ expect(TasksController.new.brainstem_plural_model_name).to eq 'tasks'
27
+ end
28
+ end
29
+
30
+ describe '.brainstem_plural_model_name' do
31
+ it 'is infered from the singular model name' do
32
+ TasksController.brainstem_model_name = 'thingy'
33
+ expect(TasksController.new.brainstem_plural_model_name).to eq 'thingies'
34
+ end
35
+
36
+ it 'can be overridden' do
37
+ TasksController.brainstem_model_name = 'thingy'
38
+ TasksController.brainstem_plural_model_name = 'thingzees'
39
+ expect(TasksController.new.brainstem_plural_model_name).to eq 'thingzees'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+ require 'brainstem/concerns/error_presentation'
3
+
4
+ describe Brainstem::Concerns::ErrorPresentation do
5
+ class ErrorsController
6
+ include Brainstem::Concerns::ErrorPresentation
7
+ end
8
+
9
+ let(:controller) { ErrorsController.new }
10
+
11
+ describe "#brainstem_system_error" do
12
+ let(:options) { { type: :other } }
13
+
14
+ it "accepts a list of messages" do
15
+ error_response = {errors: [{type: :system, message: "error1"}, {type: :system, message: "error2"}]}
16
+ expect(controller.brainstem_system_error("error1", "error2")).to eq(error_response)
17
+ expect(controller.brainstem_system_error(["error1", "error2"])).to eq(error_response)
18
+ end
19
+
20
+ it "accepts an options hash as last argument" do
21
+ error_response = {errors: [{type: :other, message: "error1"}, {type: :other, message: "error2"}]}
22
+ expect(controller.brainstem_system_error("error1", "error2", options)).to eq(error_response)
23
+ expect(controller.brainstem_system_error(["error1", "error2"], options)).to eq(error_response)
24
+ end
25
+ end
26
+
27
+ describe "#brainstem_model_error" do
28
+ context 'with a Hash or Hashes' do
29
+ it 'has a default type' do
30
+ expect(controller.brainstem_model_error({ message: 'hello', field: 'some_field' })).to eq({ errors: [ { message: 'hello', type: 'validation', field: 'some_field' } ] })
31
+ expect(controller.brainstem_model_error([{ message: 'hello1', field: 'some_field1' }, { message: 'hello2', field: 'some_field2' }])).to eq({
32
+ errors: [
33
+ { message: 'hello1', type: 'validation', field: 'some_field1' },
34
+ { message: 'hello2', type: 'validation', field: 'some_field2' }
35
+ ]
36
+ })
37
+ end
38
+
39
+ it 'can override the type' do
40
+ expect(controller.brainstem_model_error({ message: 'something', type: 'foo' })[:errors].first[:type]).to eq 'foo'
41
+ end
42
+
43
+ it 'raises an ArgumentError when no message is given' do
44
+ expect { controller.brainstem_model_error({ type: 'foo' }) }.to raise_error(ArgumentError, /message required/)
45
+ end
46
+ end
47
+
48
+ it 'can handle a String' do
49
+ expect(controller.brainstem_model_error("hello")).to eq({ errors: [ { message: 'hello', type: 'validation', field: :base } ] })
50
+ expect(controller.brainstem_model_error(["hello", "world"])).to eq({
51
+ errors: [
52
+ { message: 'hello', type: 'validation', field: :base },
53
+ { message: 'world', type: 'validation', field: :base }
54
+ ]
55
+ })
56
+ end
57
+
58
+ context 'with models' do
59
+ class Model
60
+ include ActiveModel::Validations
61
+ end
62
+
63
+ it 'can handle a single Model' do
64
+ model = Model.new
65
+ model.errors.add(:title, 'must be present')
66
+ expect(controller.brainstem_model_error(model)).to eq({ errors: [ { message: 'Title must be present', type: 'validation', field: :title, index: 0 } ] })
67
+ end
68
+
69
+ it 'can handle an array of Models' do
70
+ model1 = Model.new
71
+ model1.errors.add(:title, 'must be present')
72
+ model2 = Model.new
73
+ model2.errors.add(:foo, 'cannot be blank')
74
+ model2.errors.add(:base, 'This model is invalid')
75
+
76
+ expect(controller.brainstem_model_error([model1, model2])).to eq({
77
+ errors: [
78
+ { message: 'Title must be present', type: 'validation', field: :title, index: 0 },
79
+ { message: 'Foo cannot be blank', type: 'validation', field: :foo, index: 1 },
80
+ { message: 'This model is invalid', type: 'validation', field: :base, index: 1 },
81
+ ]
82
+ })
83
+ end
84
+
85
+ it 'can rewrite from internal to external field names' do
86
+ model = Model.new
87
+ model.errors.add(:title, 'must be present')
88
+ model.errors.add(:foo, 'cannot be blank')
89
+
90
+ expect(controller.brainstem_model_error(model, rewrite_params: { external_title: :title })).to eq({
91
+ errors: [
92
+ { message: 'Title must be present', type: 'validation', field: :external_title, index: 0 },
93
+ { message: 'Foo cannot be blank', type: 'validation', field: :foo, index: 0 },
94
+ ]
95
+ })
96
+ end
97
+
98
+ it 'handles magic ^ in error message' do
99
+ model = Model.new
100
+ model.errors.add(:title, '^Your thing must have a title')
101
+
102
+ expect(controller.brainstem_model_error(model)).to eq({
103
+ errors: [
104
+ message: 'Your thing must have a title',
105
+ type: 'validation',
106
+ field: :title,
107
+ index: 0,
108
+ ]
109
+ })
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,210 @@
1
+ require 'spec_helper'
2
+ require 'brainstem/concerns/inheritable_configuration'
3
+
4
+ describe Brainstem::Concerns::InheritableConfiguration do
5
+ let(:parent_class) do
6
+ Class.new do
7
+ include Brainstem::Concerns::InheritableConfiguration
8
+ end
9
+ end
10
+
11
+ describe '.configuration' do
12
+ it 'is inherited' do
13
+ expect(parent_class.configuration['empty']).to be_nil
14
+ parent_class.configuration['two'] = 2
15
+ parent_class.configuration['five'] = 5
16
+ expect(parent_class.configuration['two']).to eq 2
17
+ expect(parent_class.configuration['five']).to eq 5
18
+
19
+ subclass = Class.new(parent_class)
20
+ expect(subclass.configuration['empty']).to be_nil
21
+ expect(subclass.configuration['two']).to eq 2
22
+ expect(subclass.configuration['five']).to eq 5
23
+
24
+ subclass.configuration['two'] = 3
25
+ subclass.configuration['ten'] = 10
26
+ expect(subclass.configuration['empty']).to be_nil
27
+ expect(subclass.configuration['two']).to eq 3
28
+ expect(subclass.configuration['five']).to eq 5
29
+ expect(subclass.configuration['ten']).to eq 10
30
+
31
+ expect(parent_class.configuration['two']).to eq 2
32
+ expect(parent_class.configuration['five']).to eq 5
33
+ expect(parent_class.configuration['ten']).to be_nil
34
+ end
35
+
36
+ describe '#keys' do
37
+ it "returns the union of this class's keys with any parent keys" do
38
+ parent_class.configuration['1'] = :a
39
+ parent_class.configuration['2'] = :b
40
+
41
+ subclass = Class.new(parent_class)
42
+ subclass.configuration['2'] = :c
43
+ subclass.configuration['3'] = :d
44
+
45
+ subsubclass = Class.new(subclass)
46
+ subsubclass.configuration['3'] = :e
47
+ subsubclass.configuration['4'] = :f
48
+
49
+ expect(parent_class.configuration.keys).to eq ['1', '2']
50
+ expect(subclass.configuration.keys).to eq ['1', '2', '3']
51
+ expect(subsubclass.configuration.keys).to eq ['1', '2', '3', '4']
52
+
53
+ expect(parent_class.configuration['1']).to eq :a
54
+ expect(parent_class.configuration['2']).to eq :b
55
+ expect(parent_class.configuration['3']).to be_nil
56
+ expect(subclass.configuration['1']).to eq :a
57
+ expect(subclass.configuration['2']).to eq :c
58
+ expect(subclass.configuration['3']).to eq :d
59
+ expect(subclass.configuration['4']).to be_nil
60
+ expect(subsubclass.configuration['1']).to eq :a
61
+ expect(subsubclass.configuration['2']).to eq :c
62
+ expect(subsubclass.configuration['3']).to eq :e
63
+ expect(subsubclass.configuration['4']).to eq :f
64
+ end
65
+ end
66
+
67
+ describe '#nest!' do
68
+ it 'builds nested objects' do
69
+ parent_class.configuration.nest!('top_level')
70
+ expect(parent_class.configuration['top_level']).to be_a(Brainstem::DSL::Configuration)
71
+ parent_class.configuration.nest!('top_level').nest!('next_level')
72
+ expect(parent_class.configuration['top_level']['next_level']).to be_a(Brainstem::DSL::Configuration)
73
+ end
74
+
75
+ it 'is chainable' do
76
+ parent_class.configuration.nest!('top_level').nest!('sub_one')
77
+ sub_two = parent_class.configuration.nest!('top_level').nest!('sub_two')
78
+ expect(parent_class.configuration['top_level']['sub_one']).to be_a(Brainstem::DSL::Configuration)
79
+ expect(parent_class.configuration['top_level']['sub_two']).to be_a(Brainstem::DSL::Configuration)
80
+ expect(parent_class.configuration['top_level']['sub_two']).to be sub_two
81
+ end
82
+
83
+ it 'inherits nested values' do
84
+ parent_class.configuration.nest!('top_level').nest!('sub_one')
85
+ parent_class.configuration.nest!('top_level').nest!('sub_two')
86
+ parent_class.configuration['top_level']['key'] = 'value'
87
+ parent_class.configuration['top_level']['key2'] = 'value2'
88
+ parent_class.configuration['top_level']['sub_one']['sub_one_key1'] = 'sub_one_value1'
89
+ parent_class.configuration['top_level']['sub_one']['sub_one_key2'] = 'sub_one_value2'
90
+ parent_class.configuration['top_level']['sub_two']['sub_two_key'] = 'sub_two_value'
91
+
92
+ expect(parent_class.configuration['top_level']['key']).to eq 'value'
93
+ expect(parent_class.configuration['top_level']['sub_one']['sub_one_key1']).to eq 'sub_one_value1'
94
+
95
+ subclass = Class.new(parent_class)
96
+ expect(subclass.configuration['top_level']['key']).to eq 'value'
97
+ expect(subclass.configuration['top_level']['key2']).to eq 'value2'
98
+ expect(subclass.configuration['top_level']['sub_one']['sub_one_key1']).to eq 'sub_one_value1'
99
+ expect(subclass.configuration['top_level']['sub_one']['sub_one_key2']).to eq 'sub_one_value2'
100
+ expect(subclass.configuration['top_level']['sub_two']['sub_two_key']).to eq 'sub_two_value'
101
+
102
+ # These should have no affect
103
+ subclass.configuration['top_level'].nest!('sub_one')
104
+ subclass.configuration['top_level'].nest!('sub_two')
105
+
106
+ # This should add a new nested configuration
107
+ subclass.configuration['top_level'].nest!('new_nesting')['key'] = 'hello'
108
+
109
+ subclass.configuration['top_level']['key'] = 'overriden value'
110
+ subclass.configuration['top_level']['new_key'] = 'new value'
111
+ subclass.configuration['top_level']['sub_one']['sub_one_key1'] = 'overriden nested value'
112
+ subclass.configuration['top_level']['sub_one']['new_key'] = 'new nested value'
113
+
114
+ expect(subclass.configuration['top_level']['key']).to eq 'overriden value'
115
+ expect(subclass.configuration['top_level']['key2']).to eq 'value2'
116
+ expect(subclass.configuration['top_level']['new_key']).to eq 'new value'
117
+ expect(subclass.configuration['top_level']['sub_one']['sub_one_key1']).to eq 'overriden nested value'
118
+ expect(subclass.configuration['top_level']['sub_one']['sub_one_key2']).to eq 'sub_one_value2'
119
+ expect(subclass.configuration['top_level']['sub_one']['new_key']).to eq 'new nested value'
120
+ expect(subclass.configuration['top_level']['sub_two']['sub_two_key']).to eq 'sub_two_value'
121
+ expect(subclass.configuration['top_level']['new_nesting']['key']).to eq 'hello'
122
+
123
+ expect(parent_class.configuration['top_level']['key']).to eq 'value'
124
+ expect(parent_class.configuration['top_level']['key2']).to eq 'value2'
125
+ expect(parent_class.configuration['top_level']['new_key']).to be_nil
126
+ expect(parent_class.configuration['top_level']['sub_one']['sub_one_key1']).to eq 'sub_one_value1'
127
+ expect(parent_class.configuration['top_level']['sub_one']['sub_one_key2']).to eq 'sub_one_value2'
128
+ expect(parent_class.configuration['top_level']['sub_one']['new_key']).to be_nil
129
+ expect(parent_class.configuration['top_level']['sub_two']['sub_two_key']).to eq 'sub_two_value'
130
+ expect(parent_class.configuration['top_level']['new_nesting']).to be_nil
131
+
132
+ # Adding an attribute to parent later will show up in sub
133
+ parent_class.configuration['top_level']['added_later'] = 5
134
+ expect(parent_class.configuration['top_level']['added_later']).to eq 5
135
+ expect(subclass.configuration['top_level']['added_later']).to eq 5
136
+
137
+ # Changing an attribute to parent later will show up in sub
138
+ parent_class.configuration['top_level']['added_later'] = 6
139
+ expect(parent_class.configuration['top_level']['added_later']).to eq 6
140
+ expect(subclass.configuration['top_level']['added_later']).to eq 6
141
+
142
+ # Overriding an attribute in sub only affects sub.
143
+ subclass.configuration['top_level']['added_later'] = 5
144
+ expect(parent_class.configuration['top_level']['added_later']).to eq 6
145
+ expect(subclass.configuration['top_level']['added_later']).to eq 5
146
+
147
+ # Can add nesting to sub only
148
+ subclass.configuration['top_level'].nest!('only_sub')
149
+ subclass.configuration['top_level']['only_sub']['two'] = 2
150
+ expect(subclass.configuration['top_level']['only_sub']['two']).to eq 2
151
+ expect(parent_class.configuration['top_level']['only_sub']).to be_nil
152
+
153
+ # Go even deeper
154
+ subsubclass = Class.new(subclass)
155
+ expect(subsubclass.configuration['top_level']['only_sub']['two']).to eq 2
156
+ subsubclass.configuration['top_level']['only_sub']['two'] = 3
157
+ expect(subsubclass.configuration['top_level']['only_sub']['two']).to eq 3
158
+ expect(subclass.configuration['top_level']['only_sub']['two']).to eq 2
159
+ end
160
+
161
+ it 'will not override nested values' do
162
+ parent_class.configuration.nest!('top_level').nest!('sub_one')
163
+ subclass = Class.new(parent_class)
164
+ expect(lambda { subclass.configuration['top_level']['sub_one'] = 2 }).to raise_error('You cannot override a nested value')
165
+ end
166
+ end
167
+
168
+ describe '#array!' do
169
+ let!(:array) { parent_class.configuration.array!('list') }
170
+
171
+ it 'builds an InheritableAppendSet' do
172
+ expect(array).to be_a(Brainstem::DSL::Configuration::InheritableAppendSet)
173
+ expect(parent_class.configuration.array!('list')).to be array
174
+ end
175
+
176
+ it 'is inherited' do
177
+ parent_class.configuration['list'] << '2'
178
+ parent_class.configuration['list'].push 3
179
+
180
+ subclass = Class.new(parent_class)
181
+ expect(parent_class.configuration['list'].to_a).to eq ['2', 3]
182
+ expect(subclass.configuration['list'].to_a).to eq ['2', 3]
183
+
184
+ parent_class.configuration['list'].push 4
185
+ expect(parent_class.configuration['list'].to_a).to eq ['2', 3, 4]
186
+ expect(subclass.configuration['list'].to_a).to eq ['2', 3, 4]
187
+
188
+ subclass.configuration['list'].push 5
189
+ expect(parent_class.configuration['list'].to_a).to eq ['2', 3, 4]
190
+ expect(subclass.configuration['list'].to_a).to eq ['2', 3, 4, 5]
191
+
192
+ parent_class.configuration['list'].push 6
193
+ expect(parent_class.configuration['list'].to_a).to eq ['2', 3, 4, 6]
194
+ expect(subclass.configuration['list'].to_a).to eq ['2', 3, 4, 6, 5]
195
+ end
196
+
197
+ it 'will not override arrays' do
198
+ subclass = Class.new(parent_class)
199
+ expect(lambda { subclass.configuration['list'] = 2 }).to raise_error('You cannot override an inheritable array once set')
200
+ end
201
+ end
202
+ end
203
+
204
+ describe '#configuration' do
205
+ it 'is available on the instance' do
206
+ parent_class.configuration['two'] = 2
207
+ expect(parent_class.new.configuration['two']).to eq 2
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,412 @@
1
+ require 'spec_helper'
2
+ require 'brainstem/concerns/presenter_dsl'
3
+
4
+ # preload :lead_user
5
+ #
6
+ # brainstem_key :projects
7
+ #
8
+ # conditionals do
9
+ # model :title_is_hello, lambda { workspace.title == 'hello' }, 'visible when the title is hello'
10
+ # request :user_is_bob, lambda { current_user.username == 'bob' }, 'visible only to bob'
11
+ # end
12
+ #
13
+ # fields do
14
+ # field :title, :string
15
+ # field :description, :string
16
+ # field :updated_at, :datetime
17
+ # field :dynamic_title, :string, dynamic: lambda { |model| model.title }
18
+ # field :secret, :string, 'a secret, via secret_info',
19
+ # via: :secret_info,
20
+ # if: [:user_is_bob, :title_is_hello]
21
+ #
22
+ # with_options if: :user_is_bob do
23
+ # field :bob_title, :string, 'another name for the title, only for Bob',
24
+ # via: :title
25
+ # end
26
+ # fields :nested_permissions do
27
+ # field :something_title, :string, via: :title
28
+ # field :random, :number, dynamic: lambda { rand }
29
+ # end
30
+ # end
31
+ #
32
+ # associations do
33
+ # association :tasks, Task, 'The Tasks in this Workspace',
34
+ # restrict_to_only: true
35
+ # association :lead_user, User, 'The user who runs this Workspace'
36
+ # association :subtasks, Task, 'Only Tasks in this Workspace that are subtasks',
37
+ # dynamic: lambda { |workspace| workspace.tasks.where('parent_id IS NOT NULL') }
38
+ # association :something, :polymorphic
39
+ # end
40
+
41
+ describe Brainstem::Concerns::PresenterDSL do
42
+ let(:presenter_class) do
43
+ Class.new do
44
+ include Brainstem::Concerns::PresenterDSL
45
+ end
46
+ end
47
+
48
+ describe '#preload directive' do
49
+ it 'builds a list of associations to preload' do
50
+ presenter_class.preload :tasks
51
+ expect(presenter_class.configuration[:preloads].to_a).to eq [:tasks]
52
+ presenter_class.preload(lead_user: { workspaces: [:lead_user, :tasks] })
53
+ expect(presenter_class.configuration[:preloads].to_a).to eq [ :tasks, lead_user: { workspaces: [:lead_user, :tasks] } ]
54
+ end
55
+
56
+ it 'is inherited' do
57
+ presenter_class.preload :tasks
58
+ subclass = Class.new(presenter_class)
59
+ subclass.preload :lead_user
60
+ expect(presenter_class.configuration[:preloads].to_a).to eq [:tasks]
61
+ expect(subclass.configuration[:preloads].to_a).to eq [:tasks, :lead_user]
62
+ end
63
+ end
64
+
65
+ describe 'the conditional block' do
66
+ before do
67
+ presenter_class.conditionals do
68
+ model :title_is_hello, lambda { |workspace| workspace.title == 'hello' }, 'visible when the title is hello'
69
+ request :user_is_bob, lambda { current_user == 'bob' }, 'visible only to bob'
70
+ end
71
+ end
72
+
73
+ it 'is stored in the configuration' do
74
+ expect(presenter_class.configuration[:conditionals].keys).to eq %w[title_is_hello user_is_bob]
75
+ expect(presenter_class.configuration[:conditionals][:title_is_hello].action).to be_present
76
+ expect(presenter_class.configuration[:conditionals][:title_is_hello].type).to eq :model
77
+ expect(presenter_class.configuration[:conditionals][:title_is_hello].description).to eq 'visible when the title is hello'
78
+ expect(presenter_class.configuration[:conditionals][:user_is_bob].action).to be_present
79
+ expect(presenter_class.configuration[:conditionals][:user_is_bob].type).to eq :request
80
+ expect(presenter_class.configuration[:conditionals][:user_is_bob].description).to eq 'visible only to bob'
81
+ end
82
+
83
+ it 'is inherited and overridable' do
84
+ subclass = Class.new(presenter_class)
85
+ subclass.conditionals do
86
+ model :silly_conditional, lambda { rand > 0.5 }, 'visible half the time'
87
+ model :title_is_hello, lambda { |workspace| workspace.title == 'HELLO' }, 'visible when the title is hello (in all caps)'
88
+ end
89
+ expect(presenter_class.configuration[:conditionals].keys).to eq %w[title_is_hello user_is_bob]
90
+ expect(subclass.configuration[:conditionals].keys).to eq %w[title_is_hello user_is_bob silly_conditional]
91
+ expect(presenter_class.configuration[:conditionals][:title_is_hello].description).to eq "visible when the title is hello"
92
+ expect(subclass.configuration[:conditionals][:title_is_hello].description).to eq "visible when the title is hello (in all caps)"
93
+ end
94
+ end
95
+
96
+ describe 'the fields block' do
97
+ before do
98
+ presenter_class.fields do
99
+ field :updated_at, :datetime
100
+ field :dynamic_title, :string, dynamic: lambda { |model| model.title }
101
+ field :secret, :string,
102
+ via: :secret_info,
103
+ if: [:user_is_bob, :title_is_hello]
104
+
105
+ with_options if: :user_is_bob do
106
+ field :bob_title, :string, 'another name for the title, only for Bob',
107
+ via: :title
108
+ end
109
+ fields :nested_permissions do
110
+ field :something_title, :string, via: :title
111
+ field :random, :number, dynamic: lambda { rand }
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'is stored in the configuration' do
117
+ expect(presenter_class.configuration[:fields].keys).to match_array %w[updated_at dynamic_title secret bob_title nested_permissions]
118
+ expect(presenter_class.configuration[:fields][:updated_at].type).to eq :datetime
119
+ expect(presenter_class.configuration[:fields][:updated_at].description).to be_nil
120
+ expect(presenter_class.configuration[:fields][:dynamic_title].type).to eq :string
121
+ expect(presenter_class.configuration[:fields][:dynamic_title].description).to be_nil
122
+ expect(presenter_class.configuration[:fields][:dynamic_title].options[:dynamic]).to be_a(Proc)
123
+ expect(presenter_class.configuration[:fields][:secret].type).to eq :string
124
+ expect(presenter_class.configuration[:fields][:secret].description).to be_nil
125
+ expect(presenter_class.configuration[:fields][:secret].options).to eq({ via: :secret_info, if: [:user_is_bob, :title_is_hello] })
126
+ expect(presenter_class.configuration[:fields][:bob_title].type).to eq :string
127
+ expect(presenter_class.configuration[:fields][:bob_title].description).to eq 'another name for the title, only for Bob'
128
+ expect(presenter_class.configuration[:fields][:bob_title].options).to eq({ via: :title, if: [:user_is_bob] })
129
+ end
130
+
131
+ it 'handles nesting' do
132
+ expect(presenter_class.configuration[:fields][:nested_permissions][:something_title].type).to eq :string
133
+ expect(presenter_class.configuration[:fields][:nested_permissions][:something_title].options[:via]).to eq :title
134
+ expect(presenter_class.configuration[:fields][:nested_permissions][:random].options[:dynamic]).to be_a(Proc)
135
+ end
136
+
137
+ it 'is inherited and overridable' do
138
+ subclass = Class.new(presenter_class)
139
+ subclass.fields do
140
+ field :title, :string
141
+ with_options if: [:some_condition, :some_other_condition] do
142
+ field :updated_at, :datetime, 'this time I have a description and condition'
143
+ end
144
+ end
145
+ expect(presenter_class.configuration[:fields].keys).to match_array %w[updated_at dynamic_title secret bob_title nested_permissions]
146
+ expect(subclass.configuration[:fields].keys).to match_array %w[updated_at dynamic_title secret bob_title title nested_permissions]
147
+ expect(presenter_class.configuration[:fields][:updated_at].description).to be_nil
148
+ expect(presenter_class.configuration[:fields][:updated_at].options).to eq({})
149
+ expect(subclass.configuration[:fields][:updated_at].description).to eq 'this time I have a description and condition'
150
+ expect(subclass.configuration[:fields][:updated_at].options).to eq({ if: [:some_condition, :some_other_condition] })
151
+ end
152
+
153
+ it 'any :if options are combined and inherited using with_options' do
154
+ presenter_class.fields do
155
+ with_options if: :user_is_bob do
156
+ field :bob_title, :string, 'another name for the title, only for Bob',
157
+ via: :title, if: :another_condition
158
+ field :bob_title2, :string, 'another name for the title, only for Bob',
159
+ via: :title, if: :another_condition
160
+ end
161
+ end
162
+ subclass = Class.new(presenter_class)
163
+ subclass.fields do
164
+ with_options if: [:user_is_bob, :more_specific] do
165
+ field :bob_title, :string, 'another name for the title, only for Bob',
166
+ via: :title, if: [:another_condition]
167
+ field :bob_title2, :string, 'another name for the title, only for Bob',
168
+ via: :title
169
+ end
170
+ end
171
+
172
+ expect(presenter_class.configuration[:fields][:bob_title].options[:if]).to eq([:user_is_bob, :another_condition])
173
+ expect(subclass.configuration[:fields][:bob_title].options[:if]).to eq([:user_is_bob, :more_specific, :another_condition])
174
+
175
+ expect(presenter_class.configuration[:fields][:bob_title2].options[:if]).to eq([:user_is_bob, :another_condition])
176
+ expect(subclass.configuration[:fields][:bob_title2].options[:if]).to eq([:user_is_bob, :more_specific])
177
+ end
178
+
179
+ it 'allows nesting to be inherited and overridden too' do
180
+ subclass = Class.new(presenter_class)
181
+ subclass.fields do
182
+ fields :nested_permissions do
183
+ field :something_title, :number, via: :title
184
+ field :new, :string, via: :title
185
+ fields :deeper do
186
+ field :something, :string, via: :title
187
+ end
188
+ end
189
+
190
+ fields :new_nested_permissions do
191
+ field :something, :string, via: :title
192
+ end
193
+ end
194
+ expect(presenter_class.configuration[:fields].keys).to match_array %w[updated_at dynamic_title secret bob_title nested_permissions]
195
+ expect(subclass.configuration[:fields].keys).to match_array %w[updated_at dynamic_title secret bob_title nested_permissions new_nested_permissions]
196
+
197
+ expect(presenter_class.configuration[:fields][:nested_permissions][:something_title].type).to eq :string
198
+ expect(presenter_class.configuration[:fields][:nested_permissions][:random].type).to eq :number
199
+ expect(presenter_class.configuration[:fields][:nested_permissions][:new]).to be_nil
200
+ expect(presenter_class.configuration[:fields][:nested_permissions][:deeper]).to be_nil
201
+ expect(presenter_class.configuration[:fields][:new_nested_permissions]).to be_nil
202
+
203
+ expect(subclass.configuration[:fields][:nested_permissions][:something_title].type).to eq :number # changed this
204
+ expect(subclass.configuration[:fields][:nested_permissions][:random].type).to eq :number
205
+ expect(subclass.configuration[:fields][:nested_permissions][:new].type).to eq :string
206
+ expect(subclass.configuration[:fields][:nested_permissions][:deeper][:something]).to be_present
207
+ expect(subclass.configuration[:fields][:new_nested_permissions]).to be_present
208
+ end
209
+ end
210
+
211
+ describe 'the associations block' do
212
+ before do
213
+ presenter_class.associations do
214
+ association :tasks, Task, 'The Tasks in this Workspace',
215
+ restrict_to_only: true
216
+ association :subtasks, Task, 'Only Tasks in this Workspace that are subtasks',
217
+ dynamic: lambda { |workspace| workspace.tasks.where('parent_id IS NOT NULL') }
218
+ association :something, :polymorphic
219
+ end
220
+ end
221
+
222
+ it 'is stored in the configuration' do
223
+ expect(presenter_class.configuration[:associations].keys).to match_array %w[tasks subtasks something]
224
+ expect(presenter_class.configuration[:associations][:tasks].target_class).to eq Task
225
+ expect(presenter_class.configuration[:associations][:tasks].description).to eq 'The Tasks in this Workspace'
226
+ expect(presenter_class.configuration[:associations][:tasks].options).to eq({ restrict_to_only: true })
227
+ expect(presenter_class.configuration[:associations][:subtasks].target_class).to eq Task
228
+ expect(presenter_class.configuration[:associations][:subtasks].description).to eq 'Only Tasks in this Workspace that are subtasks'
229
+ expect(presenter_class.configuration[:associations][:subtasks].options.keys).to eq [:dynamic]
230
+ expect(presenter_class.configuration[:associations][:something].target_class).to eq :polymorphic
231
+ expect(presenter_class.configuration[:associations][:something].description).to be_nil
232
+ end
233
+
234
+ it 'is inherited and overridable' do
235
+ subclass = Class.new(presenter_class)
236
+ subclass.associations do
237
+ association :tasks, Task, 'The Tasks in this Workspace'
238
+ association :lead_user, User, 'The user who runs this Workspace'
239
+ end
240
+
241
+ expect(presenter_class.configuration[:associations].keys).to match_array %w[tasks subtasks something]
242
+ expect(subclass.configuration[:associations].keys).to match_array %w[tasks subtasks lead_user something]
243
+
244
+ expect(presenter_class.configuration[:associations][:tasks].options).to eq({ restrict_to_only: true })
245
+ expect(presenter_class.configuration[:associations][:lead_user]).to be_nil
246
+
247
+ expect(subclass.configuration[:associations][:tasks].options).to eq({})
248
+ expect(subclass.configuration[:associations][:lead_user].target_class).to eq User
249
+ expect(subclass.configuration[:associations][:lead_user].description).to eq 'The user who runs this Workspace'
250
+ end
251
+ end
252
+
253
+ describe ".helper" do
254
+ let(:model) { Workspace.first }
255
+
256
+ let(:presenter) do
257
+ Class.new(Brainstem::Presenter) do
258
+ helper do
259
+ def method_in_block
260
+ 'method_in_block'
261
+ end
262
+
263
+ def block_to_module
264
+ 'i am in a block, but can see ' + method_in_module
265
+ end
266
+ end
267
+
268
+ fields do
269
+ field :from_module, :string, dynamic: lambda { method_in_module }
270
+ field :from_block, :string, dynamic: lambda { method_in_block }
271
+ field :block_to_module, :string, dynamic: lambda { block_to_module }
272
+ field :module_to_block, :string, dynamic: lambda { module_to_block }
273
+ end
274
+ end
275
+ end
276
+
277
+ let(:sub_presenter) do
278
+ Class.new(presenter) do
279
+ helper do
280
+ def method_in_block
281
+ 'overridden method_in_block'
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ let(:helper_module) do
288
+ Module.new do
289
+ def method_in_module
290
+ 'method_in_module'
291
+ end
292
+
293
+ def module_to_block
294
+ 'i am in a module, but can see ' + method_in_block
295
+ end
296
+ end
297
+ end
298
+
299
+ let(:sub_helper_module) do
300
+ Module.new do
301
+ def method_in_module
302
+ 'overridden method_in_module'
303
+ end
304
+ end
305
+ end
306
+
307
+ it 'provides any methods from given blocks to the lambda' do
308
+ presenter.helper helper_module
309
+ expect(presenter.new.group_present([model]).first['from_block']).to eq 'method_in_block'
310
+ end
311
+
312
+ it 'provides any methods from given Modules to the lambda' do
313
+ presenter.helper helper_module
314
+ expect(presenter.new.group_present([model]).first['from_module']).to eq 'method_in_module'
315
+ end
316
+
317
+ it 'allows methods in modules and blocks to see each other' do
318
+ presenter.helper helper_module
319
+ expect(presenter.new.group_present([model]).first['block_to_module']).to eq 'i am in a block, but can see method_in_module'
320
+ expect(presenter.new.group_present([model]).first['module_to_block']).to eq 'i am in a module, but can see method_in_block'
321
+ end
322
+
323
+ it 'merges the blocks and modules into a combined helper' do
324
+ presenter.helper helper_module
325
+ expect(presenter.merged_helper_class.instance_methods).to include(:method_in_module, :module_to_block, :method_in_block, :block_to_module)
326
+ end
327
+
328
+ it 'can be cleaned up' do
329
+ expect(presenter.merged_helper_class.instance_methods).to include(:method_in_block)
330
+ expect(presenter.merged_helper_class.instance_methods).to_not include(:method_in_module)
331
+ presenter.helper helper_module
332
+ expect(presenter.merged_helper_class.instance_methods).to include(:method_in_block, :method_in_module)
333
+ presenter.reset!
334
+ expect(presenter.merged_helper_class.instance_methods).to_not include(:method_in_block)
335
+ expect(presenter.merged_helper_class.instance_methods).to_not include(:method_in_module)
336
+ end
337
+
338
+ it 'caches the generated class' do
339
+ class1 = presenter.merged_helper_class
340
+ class2 = presenter.merged_helper_class
341
+ expect(class1).to eq class2
342
+ presenter.helper helper_module
343
+ class3 = presenter.merged_helper_class
344
+ class4 = presenter.merged_helper_class
345
+ expect(class1).not_to eq class3
346
+ expect(class3).to eq class4
347
+ end
348
+
349
+ it 'is inheritable' do
350
+ presenter.helper helper_module
351
+ expect(sub_presenter.new.group_present([model]).first['from_block']).to eq 'overridden method_in_block'
352
+ expect(sub_presenter.new.group_present([model]).first['from_module']).to eq 'method_in_module'
353
+ expect(sub_presenter.new.group_present([model]).first['block_to_module']).to eq 'i am in a block, but can see method_in_module'
354
+ expect(sub_presenter.new.group_present([model]).first['module_to_block']).to eq 'i am in a module, but can see overridden method_in_block'
355
+ sub_presenter.helper sub_helper_module
356
+ expect(sub_presenter.new.group_present([model]).first['from_module']).to eq 'overridden method_in_module'
357
+ expect(sub_presenter.new.group_present([model]).first['block_to_module']).to eq 'i am in a block, but can see overridden method_in_module'
358
+ expect(presenter.new.group_present([model]).first['from_module']).to eq 'method_in_module'
359
+ expect(presenter.new.group_present([model]).first['block_to_module']).to eq 'i am in a block, but can see method_in_module'
360
+ end
361
+
362
+ it 'caches the generated classes with inheritance' do
363
+ class1 = presenter.merged_helper_class
364
+ class2 = sub_presenter.merged_helper_class
365
+ expect(presenter.merged_helper_class).to eq class1
366
+ expect(sub_presenter.merged_helper_class).to eq class2
367
+
368
+ presenter.helper helper_module
369
+
370
+ expect(presenter.merged_helper_class).not_to eq class1
371
+ expect(sub_presenter.merged_helper_class).not_to eq class2
372
+ end
373
+ end
374
+
375
+ describe ".filter" do
376
+ it "creates an entry in the filters configuration" do
377
+ presenter_class.filter(:foo, :default => true) { 1 }
378
+ expect(presenter_class.configuration[:filters][:foo][0]).to eq({"default" => true})
379
+ expect(presenter_class.configuration[:filters][:foo][1]).to be_a(Proc)
380
+ end
381
+
382
+ it "accepts names without blocks" do
383
+ presenter_class.filter(:foo)
384
+ expect(presenter_class.configuration[:filters][:foo][1]).to be_nil
385
+ end
386
+ end
387
+
388
+ describe ".search" do
389
+ it "creates an entry in the search configuration" do
390
+ presenter_class.search do end
391
+ expect(presenter_class.configuration[:search]).to be_a(Proc)
392
+ end
393
+ end
394
+
395
+ describe ".brainstem_key" do
396
+ before do
397
+ presenter_class.brainstem_key(:foo)
398
+ end
399
+
400
+ it "creates an entry in the brainstem_key configuration" do
401
+ expect(presenter_class.configuration[:brainstem_key]).to eq('foo')
402
+ end
403
+
404
+ it 'is inherited and overridable' do
405
+ subclass = Class.new(presenter_class)
406
+ expect(subclass.configuration[:brainstem_key]).to eq('foo')
407
+ subclass.brainstem_key(:bar)
408
+ expect(subclass.configuration[:brainstem_key]).to eq('bar')
409
+ expect(presenter_class.configuration[:brainstem_key]).to eq('foo')
410
+ end
411
+ end
412
+ end