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.
- checksums.yaml +4 -4
- data/.gitignore +22 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -363
- data/Rakefile +6 -0
- data/decent_exposure.gemspec +30 -0
- data/decent_exposure.png +0 -0
- data/hashrocket_logo.png +0 -0
- data/lib/decent_exposure.rb +13 -4
- data/lib/decent_exposure/attribute.rb +55 -0
- data/lib/decent_exposure/behavior.rb +100 -0
- data/lib/decent_exposure/context.rb +61 -0
- data/lib/decent_exposure/controller.rb +53 -0
- data/lib/decent_exposure/exposure.rb +199 -9
- data/lib/decent_exposure/flow.rb +89 -0
- data/lib/decent_exposure/version.rb +2 -2
- data/spec/controller_spec.rb +374 -0
- data/spec/integration_spec.rb +26 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/rails_app.rb +37 -0
- metadata +63 -41
- data/lib/decent_exposure/active_record_strategy.rb +0 -84
- data/lib/decent_exposure/active_record_with_eager_attributes_strategy.rb +0 -8
- data/lib/decent_exposure/configuration.rb +0 -19
- data/lib/decent_exposure/constant_resolver.rb +0 -34
- data/lib/decent_exposure/error.rb +0 -4
- data/lib/decent_exposure/expose.rb +0 -62
- data/lib/decent_exposure/inflector.rb +0 -39
- data/lib/decent_exposure/strategies/assign_from_method.rb +0 -20
- data/lib/decent_exposure/strategies/assign_from_params.rb +0 -24
- data/lib/decent_exposure/strategizer.rb +0 -54
- data/lib/decent_exposure/strategy.rb +0 -47
- data/lib/decent_exposure/strong_parameters_strategy.rb +0 -8
@@ -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
|
2
|
-
VERSION = "
|
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
|