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.
- checksums.yaml +5 -13
- data/CHANGELOG.md +16 -2
- data/Gemfile.lock +51 -36
- data/README.md +531 -110
- data/brainstem.gemspec +6 -2
- data/lib/brainstem.rb +25 -9
- data/lib/brainstem/concerns/controller_param_management.rb +22 -0
- data/lib/brainstem/concerns/error_presentation.rb +58 -0
- data/lib/brainstem/concerns/inheritable_configuration.rb +29 -0
- data/lib/brainstem/concerns/lookup.rb +30 -0
- data/lib/brainstem/concerns/presenter_dsl.rb +111 -0
- data/lib/brainstem/controller_methods.rb +17 -8
- data/lib/brainstem/dsl/association.rb +55 -0
- data/lib/brainstem/dsl/associations_block.rb +12 -0
- data/lib/brainstem/dsl/base_block.rb +31 -0
- data/lib/brainstem/dsl/conditional.rb +25 -0
- data/lib/brainstem/dsl/conditionals_block.rb +15 -0
- data/lib/brainstem/dsl/configuration.rb +112 -0
- data/lib/brainstem/dsl/field.rb +68 -0
- data/lib/brainstem/dsl/fields_block.rb +25 -0
- data/lib/brainstem/preloader.rb +98 -0
- data/lib/brainstem/presenter.rb +325 -134
- data/lib/brainstem/presenter_collection.rb +82 -286
- data/lib/brainstem/presenter_validator.rb +96 -0
- data/lib/brainstem/query_strategies/README.md +107 -0
- data/lib/brainstem/query_strategies/base_strategy.rb +62 -0
- data/lib/brainstem/query_strategies/filter_and_search.rb +50 -0
- data/lib/brainstem/query_strategies/filter_or_search.rb +103 -0
- data/lib/brainstem/test_helpers.rb +5 -1
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/concerns/controller_param_management_spec.rb +42 -0
- data/spec/brainstem/concerns/error_presentation_spec.rb +113 -0
- data/spec/brainstem/concerns/inheritable_configuration_spec.rb +210 -0
- data/spec/brainstem/concerns/presenter_dsl_spec.rb +412 -0
- data/spec/brainstem/controller_methods_spec.rb +15 -27
- data/spec/brainstem/dsl/association_spec.rb +123 -0
- data/spec/brainstem/dsl/conditional_spec.rb +93 -0
- data/spec/brainstem/dsl/configuration_spec.rb +1 -0
- data/spec/brainstem/dsl/field_spec.rb +212 -0
- data/spec/brainstem/preloader_spec.rb +137 -0
- data/spec/brainstem/presenter_collection_spec.rb +565 -244
- data/spec/brainstem/presenter_spec.rb +726 -167
- data/spec/brainstem/presenter_validator_spec.rb +209 -0
- data/spec/brainstem/query_strategies/filter_and_search_spec.rb +46 -0
- data/spec/brainstem/query_strategies/filter_or_search_spec.rb +45 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/spec_helpers/db.rb +32 -65
- data/spec/spec_helpers/presenters.rb +124 -29
- data/spec/spec_helpers/rr.rb +11 -0
- data/spec/spec_helpers/schema.rb +115 -0
- metadata +126 -30
- data/lib/brainstem/association_field.rb +0 -53
- data/lib/brainstem/engine.rb +0 -4
- data/pkg/brainstem-0.2.5.gem +0 -0
- data/pkg/brainstem-0.2.6.gem +0 -0
- 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
|