brainstem 0.2.6.1 → 1.0.0.pre.1

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