decent_exposure 2.3.3 → 3.0.0.beta1

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.
@@ -0,0 +1,89 @@
1
+ module DecentExposure
2
+ class Flow
3
+ attr_reader :controller, :options, :name
4
+
5
+ # Public: Initialize a Flow. This object responds to missing
6
+ # methods errors and attempts to delegate them to other objects.
7
+ #
8
+ # controller - The Controller class where the method was called.
9
+ # options - The options Hash of the Exposure instance being called.
10
+ # name - The String name of the Exposure instance.
11
+ def initialize(controller, options)
12
+ @controller = controller
13
+ @options = options
14
+ @name = options.fetch(:name)
15
+ end
16
+
17
+ # Public: Attempts to re-delegate a method missing to the
18
+ # supplied block or the Behavior object.
19
+ #
20
+ # name - The String name of the Exposure instance.
21
+ # *args - The arguments given for the missing method.
22
+ # block - The Proc invoked by the method.
23
+ def method_missing(name, *args, &block)
24
+ if respond_to_missing?(name)
25
+ handle_flow_method(name, *args, &block)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ # Public: Checks if the Behavior class can handle the missing method.
32
+ #
33
+ # method_name - The name of method that has been called.
34
+ # include_private - Prevents this method from catching calls to private
35
+ # method (default: false).
36
+ def respond_to_missing?(method_name, include_private = false)
37
+ Behavior.method_defined?(method_name) || super
38
+ end
39
+
40
+ private
41
+
42
+ delegate :params, to: :controller
43
+
44
+ def get_request?
45
+ controller.request.get?
46
+ end
47
+
48
+ def params_method_name
49
+ options.fetch(:build_params_method){ "#{name}_params" }
50
+ end
51
+
52
+ def handle_flow_method(name, *args, &block)
53
+ fetch_ivar name do
54
+ if options.key?(name)
55
+ handle_options_override(name, *args, &block)
56
+ else
57
+ handle_default_flow_method(name, *args, &block)
58
+ end
59
+ end
60
+ end
61
+
62
+ def handle_options_override(name, *args)
63
+ value = options[name]
64
+
65
+ if Proc === value
66
+ args = args.first(value.parameters.length)
67
+ controller.instance_exec(*args, &value)
68
+ else
69
+ fail ArgumentError, "Can't handle #{name.inspect} => #{value.inspect} option"
70
+ end
71
+ end
72
+
73
+ def handle_default_flow_method(name, *args, &block)
74
+ method = Behavior.instance_method(name)
75
+ method.bind(self).call(*args, &block)
76
+ end
77
+
78
+
79
+ def fetch_ivar(name)
80
+ ivar_name = "@#{name}"
81
+
82
+ if instance_variable_defined?(ivar_name)
83
+ instance_variable_get(ivar_name)
84
+ else
85
+ instance_variable_set(ivar_name, yield)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,3 @@
1
- module DecentExposure #:nodoc
2
- VERSION = "2.3.3"
1
+ module DecentExposure
2
+ VERSION = "3.0.0.beta1"
3
3
  end
@@ -0,0 +1,374 @@
1
+ require "spec_helper"
2
+
3
+ describe DecentExposure::Controller do
4
+ class Thing; end
5
+ class DifferentThing; end
6
+
7
+ class BaseController
8
+ def self.helper_method(*); end
9
+
10
+ def params
11
+ @params ||= HashWithIndifferentAccess.new
12
+ end
13
+ end
14
+
15
+ let(:controller_klass) do
16
+ Class.new(BaseController) do
17
+ include DecentExposure::Controller
18
+ end
19
+ end
20
+
21
+ let(:request){ double("Request") }
22
+ let(:controller){ controller_klass.new }
23
+ before{ allow(controller).to receive(:request){ request } }
24
+
25
+ %w[expose expose! exposure_config].each do |method_name|
26
+ define_method method_name do |*args, &block|
27
+ controller_klass.send method_name, *args, &block
28
+ end
29
+ end
30
+
31
+ context "getter/setter methods" do
32
+ before{ expose :thing }
33
+
34
+ it "defines getter method" do
35
+ expect(controller).to respond_to(:thing)
36
+ end
37
+
38
+ it "defines setter method" do
39
+ expect(controller).to respond_to(:thing=).with(1).argument
40
+ end
41
+ end
42
+
43
+ context "helper methods" do
44
+ it "exposes getter and setter as controller helper methods" do
45
+ expect(controller_klass).to receive(:helper_method).with(:thing, :thing=)
46
+ expose :thing
47
+ end
48
+ end
49
+
50
+ context ".expose!" do
51
+ it "supports eager expose" do
52
+ expect(controller_klass).to receive(:before_action).with(:thing)
53
+ expose! :thing
54
+ end
55
+ end
56
+
57
+ context ".exposure_config" do
58
+ it "subclass configration doesn't propagate to superclass" do
59
+ controller_subklass = Class.new(controller_klass)
60
+ controller_klass.exposure_config :foo, :bar
61
+ controller_subklass.exposure_config :foo, :lol
62
+ controller_subklass.exposure_config :fizz, :buzz
63
+ expect(controller_subklass.exposure_configuration).to eq(foo: :lol, fizz: :buzz)
64
+ expect(controller_klass.exposure_configuration).to eq(foo: :bar)
65
+ end
66
+
67
+ context "applying" do
68
+ let(:thing){ double("Thing") }
69
+
70
+ before do
71
+ exposure_config :sluggable, find_by: :slug
72
+ exposure_config :weird_id_name, id: :check_this_out
73
+ exposure_config :another_id_name, id: :whee
74
+ exposure_config :multi, find_by: :slug, id: :check_this_out
75
+ controller.params.merge! check_this_out: "foo", whee: "wut"
76
+ end
77
+
78
+ after{ expect(controller.thing).to eq(thing) }
79
+
80
+ it "can be reused later" do
81
+ expose :thing, with: :weird_id_name
82
+ expect(Thing).to receive(:find).with("foo").and_return(thing)
83
+ end
84
+
85
+ it "can apply multple configs at once" do
86
+ expose :thing, with: [:weird_id_name, :sluggable]
87
+ expect(Thing).to receive(:find_by!).with(slug: "foo").and_return(thing)
88
+ end
89
+
90
+ it "applies multiple configs in a correct order" do
91
+ expose :thing, with: [:another_id_name, :weird_id_name]
92
+ expect(Thing).to receive(:find).with("wut").and_return(thing)
93
+ end
94
+
95
+ it "can apply multiple options in a config" do
96
+ expose :thing, with: :multi
97
+ expect(Thing).to receive(:find_by!).with(slug: "foo").and_return(thing)
98
+ end
99
+
100
+ it "applies multiple configs with multiple options in a correct order" do
101
+ expose :thing, with: [:another_id_name, :multi]
102
+ expect(Thing).to receive(:find_by!).with(slug: "wut").and_return(thing)
103
+ end
104
+ end
105
+ end
106
+
107
+ context "with block" do
108
+ before{ expose(:thing){ compute_thing } }
109
+
110
+ it "executes block to calculate the value" do
111
+ allow(controller).to receive(:compute_thing).and_return(42)
112
+ expect(controller.thing).to eq(42)
113
+ end
114
+
115
+ it "executes the block once and memoizes the result" do
116
+ expect(controller).to receive(:compute_thing).once.and_return(42)
117
+ 10.times{ controller.thing }
118
+ end
119
+
120
+ it "allows setting value directly" do
121
+ expect(controller).to_not receive(:compute_thing)
122
+ controller.thing = :foobar
123
+ expect(controller.thing).to eq(:foobar)
124
+ end
125
+
126
+ it "throws and error when providing options with block" do
127
+ action = ->{ expose(:thing, id: :some_id){ some_code } }
128
+ expect(&action).to raise_error(ArgumentError, "Using :fetch option with other options doesn't make sense")
129
+ end
130
+ end
131
+
132
+ context "passing fetch block as an argument instead of block" do
133
+ it "is equivalent to passing block" do
134
+ expose :thing, ->{ compute_thing }
135
+ expect(controller).to receive(:compute_thing).and_return(42)
136
+ expect(controller.thing).to eq(42)
137
+ end
138
+
139
+ it "throws an error when passing both block and block-argument" do
140
+ action = ->{ expose(:thing, ->{}){} }
141
+ expect(&action).to raise_error(ArgumentError, "Fetch block is already defined")
142
+ end
143
+ end
144
+
145
+ context "passing fetch block as a symbol" do
146
+ it "is equivalent to passing a block alling controller method" do
147
+ expose :thing, :calculate_thing_in_controller
148
+ expect(controller).to receive(:calculate_thing_in_controller).and_return(42)
149
+ expect(controller.thing).to eq(42)
150
+ end
151
+ end
152
+
153
+ context "redefine fetch" do
154
+ before do
155
+ expose :thing, fetch: ->{ compute_thing }
156
+ allow(controller).to receive(:compute_thing).and_return(42)
157
+ end
158
+
159
+ it "uses provided fetch proc instead of default" do
160
+ expect(controller.thing).to eq(42)
161
+ end
162
+ end
163
+
164
+ context "default behaviour" do
165
+ context "build" do
166
+ let(:thing){ double("Thing") }
167
+
168
+ after{ expect(controller.thing).to eq(thing) }
169
+
170
+ context "params method is not available" do
171
+ it "builds a new instance with empty hash" do
172
+ expose :thing
173
+ expect(Thing).to receive(:new).with({}).and_return(thing)
174
+ end
175
+ end
176
+
177
+ context "params method is available" do
178
+ it "ignores params on get request" do
179
+ expose :thing
180
+ expect(request).to receive(:get?).and_return(true)
181
+ expect(controller).not_to receive(:thing_params)
182
+ expect(Thing).to receive(:new).with({}).and_return(thing)
183
+ end
184
+
185
+ it "uses params method on non-get request" do
186
+ expose :thing
187
+ expect(request).to receive(:get?).and_return(false)
188
+ expect(Thing).to receive(:new).with(foo: :bar).and_return(thing)
189
+ expect(controller).to receive(:thing_params).and_return(foo: :bar)
190
+ end
191
+
192
+ it "can use custom params method name" do
193
+ expose :thing, build_params: :custom_params_method_name
194
+ expect(request).to receive(:get?).and_return(false)
195
+ expect(Thing).to receive(:new).with(foo: :bar).and_return(thing)
196
+ expect(controller).to receive(:custom_params_method_name).and_return(foo: :bar)
197
+ end
198
+
199
+ it "can use custom build params" do
200
+ expose :thing, build_params: ->{ foobar }
201
+ expect(controller).to receive(:foobar).and_return(42)
202
+ expect(Thing).to receive(:new).with(42).and_return(thing)
203
+ end
204
+ end
205
+ end
206
+
207
+ context "find" do
208
+ before do
209
+ expose :thing, model: :different_thing
210
+ expect(DifferentThing).to receive(:find).with(10)
211
+ end
212
+
213
+ after{ controller.thing }
214
+
215
+ it "checks params[:different_thing_id] first" do
216
+ controller.params.merge! different_thing_id: 10, thing_id: 11, id: 12
217
+ end
218
+ it "checks params[:thing_id] second" do
219
+ controller.params.merge! thing_id: 10, id: 11
220
+ end
221
+
222
+ it "checks params[:id] in the end" do
223
+ controller.params.merge! id: 10
224
+ end
225
+ end
226
+ end
227
+
228
+ context "find_by" do
229
+ it "throws and error when using with :find" do
230
+ action = ->{ expose :thing, find: :foo, find_by: :bar }
231
+ expect(&action).to raise_error(ArgumentError, "Using :find_by option with :find doesn't make sense")
232
+ end
233
+
234
+ it "allows to specify what attribute to use for find" do
235
+ expect(Thing).to receive(:find_by!).with(foo: 10).and_return(42)
236
+ expose :thing, find_by: :foo
237
+ controller.params.merge! id: 10
238
+ expect(controller.thing).to eq(42)
239
+ end
240
+ end
241
+
242
+ context "parent option" do
243
+ context "with scope/model options" do
244
+ it "throws an error when used with scope option" do
245
+ action = ->{ expose :thing, scope: :foo, parent: :something }
246
+ expect(&action).to raise_error(ArgumentError, "Using :parent option with :scope doesn't make sense")
247
+ end
248
+
249
+ it "throws an error when used with model option" do
250
+ action = ->{ expose :thing, model: :foo, parent: :something }
251
+ expect(&action).to raise_error(ArgumentError, "Using :parent option with :model doesn't make sense")
252
+ end
253
+ end
254
+
255
+ context "build/find" do
256
+ let(:current_user){ double("User") }
257
+ let(:scope){ double("Scope") }
258
+
259
+ before do
260
+ expect(controller).to receive(:current_user).and_return(current_user)
261
+ expect(current_user).to receive(:things).and_return(scope)
262
+ expose :thing, parent: :current_user
263
+ end
264
+
265
+ after{ expect(controller.thing).to eq(42) }
266
+
267
+ it "sets the scope to belong to parent defined by controller method" do
268
+ expect(scope).to receive(:new).with({}).and_return(42)
269
+ end
270
+
271
+ it "scopes the find to proper scope" do
272
+ controller.params.merge! thing_id: 10
273
+ expect(scope).to receive(:find).with(10).and_return(42)
274
+ end
275
+ end
276
+ end
277
+
278
+ context "override model" do
279
+ let(:different_thing){ double("DifferentThing") }
280
+ before{ expect(DifferentThing).to receive(:new).with({}).and_return(different_thing) }
281
+ after{ expect(controller.thing).to eq(different_thing) }
282
+
283
+ it "allows overriding model class with proc" do
284
+ expose :thing, model: ->{ DifferentThing }
285
+ end
286
+
287
+ it "allows overriding model with class" do
288
+ expose :thing, model: DifferentThing
289
+ end
290
+
291
+ it "allows overriding model class with symbol" do
292
+ expose :thing, model: :different_thing
293
+ end
294
+
295
+ it "allows overriding model class with string" do
296
+ expose :thing, model: "DifferentThing"
297
+ end
298
+ end
299
+
300
+ context "override scope" do
301
+ it "allows overriding scope with proc" do
302
+ scope = double("Scope")
303
+ expose :thing, scope: ->{ scope }
304
+ expect(scope).to receive(:new).and_return(42)
305
+ expect(controller.thing).to eq(42)
306
+ end
307
+
308
+ it "allows overriding model scope using symbol" do
309
+ scope = double("Scope")
310
+ expect(Thing).to receive(:custom_scope).and_return(scope)
311
+ expect(scope).to receive(:new).and_return(42)
312
+ expose :thing, scope: :custom_scope
313
+ expect(controller.thing).to eq(42)
314
+ end
315
+ end
316
+
317
+ context "override id" do
318
+ after do
319
+ expect(Thing).to receive(:find).with(42)
320
+ controller.thing
321
+ end
322
+
323
+ it "allows overriding id with proc" do
324
+ expose :thing, id: ->{ get_thing_id_somehow }
325
+ expect(controller).to receive(:get_thing_id_somehow).and_return(42)
326
+ end
327
+
328
+ it "allows overriding id with symbol" do
329
+ expose :thing, id: :custom_thing_id
330
+ controller.params.merge! thing_id: 10, custom_thing_id: 42
331
+ end
332
+
333
+ it "allows overriding id with an array of symbols" do
334
+ expose :thing, id: %w[non-existent-id lolwut another_id_param]
335
+ controller.params.merge! another_id_param: 42
336
+ end
337
+ end
338
+
339
+ context "override decorator" do
340
+ it "allows specify decorator" do
341
+ expose :thing, decorate: ->(thing){ decorate(thing) }
342
+ thing = double("Thing")
343
+ expect(Thing).to receive(:new).with({}).and_return(thing)
344
+ expect(controller).to receive(:decorate).with(thing)
345
+ controller.thing
346
+ end
347
+ end
348
+
349
+ context "from option" do
350
+ it "allows scope to be called from method" do
351
+ comments = double("Comments")
352
+ post = double("Post", comments: comments)
353
+ allow(controller).to receive(:post).and_return(post)
354
+ expose :comments, from: :post
355
+
356
+ expect(controller.comments).to eq(comments)
357
+ end
358
+
359
+ it "should throw error when used with other options" do
360
+ action = ->{ expose :thing, from: :foo, parent: :bar }
361
+ expect(&action).to raise_error(ArgumentError, "Using :from option with other options doesn't make sense")
362
+ end
363
+
364
+ it "should still work with decorate option" do
365
+ decorated_thing = double("DecoratedThing")
366
+ thing = double("Thing")
367
+ foo = double("Foo", thing: thing)
368
+ expect(controller).to receive(:foo).and_return(foo)
369
+ expect(controller).to receive(:decorate).with(thing).and_return(decorated_thing)
370
+ expose :thing, from: :foo, decorate: ->(thing){ decorate(thing) }
371
+ expect(controller.thing).to eq(decorated_thing)
372
+ end
373
+ end
374
+ end