context_exposer 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -1
  3. data/README.md +186 -16
  4. data/lib/context_exposer.rb +15 -0
  5. data/lib/context_exposer/base_controller.rb +67 -47
  6. data/lib/context_exposer/cached_resource_controller.rb +10 -0
  7. data/lib/context_exposer/integrations.rb +8 -0
  8. data/lib/context_exposer/integrations/base.rb +15 -0
  9. data/lib/context_exposer/integrations/key_filter.rb +30 -0
  10. data/lib/context_exposer/integrations/with_decent_exposure.rb +19 -0
  11. data/lib/context_exposer/integrations/with_decorates_before.rb +31 -0
  12. data/lib/context_exposer/integrations/with_instance_vars.rb +19 -0
  13. data/lib/context_exposer/macros.rb +17 -0
  14. data/lib/context_exposer/patch/decorates_before_rendering.rb +179 -0
  15. data/lib/context_exposer/rails_config.rb +3 -0
  16. data/lib/context_exposer/resource_controller.rb +30 -8
  17. data/lib/context_exposer/version.rb +1 -1
  18. data/lib/context_exposer/view_context.rb +1 -1
  19. data/spec/app/items_spec.rb +36 -0
  20. data/spec/app/posts_spec.rb +29 -1
  21. data/spec/context_exposer/expose_resource_spec.rb +51 -38
  22. data/spec/context_exposer/expose_spec.rb +11 -10
  23. data/spec/dummy/app/controllers/items_controller.rb +12 -0
  24. data/spec/dummy/app/controllers/posts_controller.rb +3 -4
  25. data/spec/dummy/app/views/items/index.html.erb +2 -0
  26. data/spec/dummy/app/views/items/show.html.erb +2 -0
  27. data/spec/dummy/app/views/posts/index.html.erb +2 -1
  28. data/spec/dummy/app/views/posts/show.html.erb +2 -1
  29. data/spec/dummy/config/initializers/context_exposer.rb +1 -0
  30. data/spec/dummy/config/routes.rb +1 -57
  31. data/spec/dummy/log/test.log +483 -0
  32. data/spec/dummy_spec_helper.rb +82 -0
  33. data/spec/spec_helper.rb +13 -1
  34. data/spec/support/decorators/base_decorator.rb +22 -0
  35. data/spec/support/decorators/item_decorator.rb +11 -0
  36. data/spec/support/decorators/post_decorator.rb +5 -0
  37. data/spec/support/models/base.rb +18 -0
  38. data/spec/support/models/base/clazz_methods.rb +29 -0
  39. data/spec/support/models/item.rb +9 -0
  40. data/spec/support/models/post.rb +5 -0
  41. metadata +38 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2452c12a649755432297fcee4e4fefb40b864313
4
- data.tar.gz: 8ace695b9b00d9f114874aa9b5719064c64a0c83
3
+ metadata.gz: cff7846013a27d16f62e455b571af3336ea88531
4
+ data.tar.gz: d7b782a83befc4da2b8ffa9a145eff505734d303
5
5
  SHA512:
6
- metadata.gz: 78721b922904ab3222e41024042691c3998103414314a5f4adf75bf8b34fd6c9a529c7acee68c23af948219b3577727276ffb1c617fd8d1ed21dc93a92484d47
7
- data.tar.gz: c0bcbf8cb1781b7fc39f0b1d6246c7b11f931056925f64becd31ab19f3e76606b996b4faa5e67470ec8c407c4dc86bb9ba4c369fe7bfae23330a33f1914bec34
6
+ metadata.gz: 47926e8db084652454e11b10b82e949618809ed3a1b9141c98b5830dae93a5e771fe79aca837a7a1520062710b9c3fe5f4a7cf63366942048db414391b7e1f36
7
+ data.tar.gz: 191e35391a9271b9cc3a3dea8e2429fee8ebb0c6788ade0feb2f9c449303c280fc6bcf26c3d2bf3151db1dcc94db809fff341c87fcd9a91a0717a885785e7553
data/Gemfile CHANGED
@@ -4,5 +4,17 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'rails', '>= 3.1'
7
+ gem "rspec", '>= 2.0', group: [:test, :development]
7
8
  gem "rspec-rails", '>= 2.0', group: [:test, :development]
8
- gem 'pry', group: [:development]
9
+ gem 'pry', group: [:development]
10
+
11
+ group :test do
12
+ gem 'capybara', '>= 2.0'
13
+ gem 'spork-rails', '>= 3.0'
14
+
15
+ gem 'decent_exposure'
16
+ gem 'decorates_before_rendering', github: 'ohwillie/decorates_before_rendering'
17
+
18
+ # gem 'factory_girl_rails', '>= 3.0'
19
+ # gem 'database_cleaner', '>= 0.7'
20
+ end
data/README.md CHANGED
@@ -7,6 +7,14 @@ No more pollution of the View with content helper methods or even worse, instanc
7
7
 
8
8
  The Context object will by default be an instance of `ContextExposer::ViewContext`, but you can subclass this baseclass to add you own logic for more complex scenarios. This also allows for a more modular approach, where you can easily share or subclass logic between different view contexts. Nice!
9
9
 
10
+ The gem comes with integrations ready for easy migration or symbiosis with existing strategies (and gems), such as:
11
+
12
+ * exposing of instance variables (Rails default strategy)
13
+ * decent_exposure gem (expose methods)
14
+ * decorates_before_rendering gem (expose decorated instance vars)
15
+
16
+ For more on integration (and migration path) see below ;)
17
+
10
18
  ## Installation
11
19
 
12
20
  Add this line to your application's Gemfile:
@@ -36,15 +44,44 @@ class PostsController < ActionController::Base
36
44
  end
37
45
  ```
38
46
 
47
+ The view will have the methods exposed and available on the `ctx` object.
48
+
39
49
  HAML view example
40
50
 
41
51
  ```haml
42
52
  %h1 Posts
43
- = context.posts.each do |post|
53
+ = ctx.posts.each do |post|
44
54
  %h2 = post.name
45
55
  ```
46
56
 
47
- You can also define your own subclass of `ViewContext` and designate an instance of this custom class as your "exposed" target, via `view_context_class`method.
57
+ You can also have the exposed methods automatically cache the result in an instance variable, by using the `expose_cached` variant.
58
+
59
+ ```ruby
60
+ class PostsController < ActionController::Base
61
+ include ContextExposer::BaseController
62
+
63
+ expose_cached(:post) { Post.find params[:id] }
64
+ expose_cached(:posts) { Post.find params[:id] }
65
+ end
66
+ ```
67
+
68
+ This is especially useful if used in combination with `decorates_before_rendering`, which only works on cached objects.
69
+
70
+ ## Macros
71
+
72
+ You can also choose to use the class macros made available on `ActionController::Base` as Rails loads.
73
+
74
+ Use `:base` or `resource` or your custom extension to include the ContextExposer controller module of your choice. The macro `context_exposer :base` is equivalent to writing `include ContextExposer::BaseController`
75
+
76
+ ```ruby
77
+ class PostsController < ActionController::Base
78
+ context_exposer :base
79
+ ```
80
+
81
+ ## Sublclassing and customizing the ViewContext
82
+
83
+ You can also define your own subclass of `ViewContext` and designate an instance of this custom class as your "exposed" target, via `view_ctx_class`method.
84
+ You can also override the class method of the same name for custom class name construction behavior ;)
48
85
 
49
86
  Example:
50
87
 
@@ -52,10 +89,16 @@ Example:
52
89
  class PostsController < ActionController::Base
53
90
  include ContextExposer::BaseController
54
91
 
55
- view_context_class :posts_view_context
92
+ view_ctx_class :posts_view_context
56
93
 
57
- exposed(:post) { Post.find params[:id] }
58
- exposed(:posts) { Post.all }
94
+ # One model instance
95
+ exposed(:post) { Post.find params[:id] }
96
+
97
+ # Relation (for further scoping or lazy load)
98
+ exposed(:posts) { Post.all }
99
+
100
+ # Array of model instances
101
+ exposed(:posts_list) { Post.all.to_a }
59
102
  end
60
103
  ```
61
104
 
@@ -90,7 +133,7 @@ HAML view example
90
133
 
91
134
  ```haml
92
135
  %h1 Admin Posts
93
- = context.admin_posts.each do |post|
136
+ = ctx.admin_posts.each do |post|
94
137
  %h2 = post.name
95
138
  ```
96
139
 
@@ -101,17 +144,26 @@ This approach opens up many new exciting ways to slice and dice your logic in a
101
144
 
102
145
  ### ResourceController
103
146
 
104
- The `ResourceController` automatically sets up the typical singular and plural-form resource helpers.
147
+ The `ResourceController` automatically sets up the typical singular and plural-form resource helpers. For example for PostsController:
148
+
149
+ * `post` - one Post instance
150
+ * `posts` - Search Relatation (for lazy load or further scoping)
151
+ * `posts_list` - Array of Post instances
105
152
 
106
153
  This simplifies the above `PostsController` example to this:
107
154
 
108
155
  ```ruby
109
156
  class PostsController < ActionController::Base
157
+ # alternatively: context_exposer :resource
110
158
  include ContextExposer::ResourceController
159
+
160
+ expose_resources :all
111
161
  end
112
162
  ```
113
163
 
114
- `ResourceController` uses the following internal logic for its default functionality. You can override these methods to customize your behavior as needed.
164
+ The macro `expose_resources` optionally takes a list of the types of resource you want to expose. Valid types are `:one`, `:many` and `:list` respectively (for fx: `post`, `posts` and `posts_list`).
165
+
166
+ `ContextExposer::ResourceController` uses the following internal logic for its default functionality. You can override these methods to customize your behavior as needed.
115
167
 
116
168
  ```ruby
117
169
  module ContextExposer::ResourceController
@@ -132,22 +184,75 @@ module ContextExposer::ResourceController
132
184
  end
133
185
  ```
134
186
 
135
- ## Integration with decent_exposure gem
187
+ Tip: You can create reusable module and then include your custom ResourceController.
188
+
189
+ ```ruby
190
+ module NamedResourceController
191
+ extend ActiveSupport::Concern
192
+ include ContextExposer::ResourceController
193
+
194
+ protected
195
+
196
+ def resource_id
197
+ params[:name]
198
+ end
199
+ end
200
+ ```
201
+
202
+ ```ruby
203
+ class PostsController < ActionController::Base
204
+ include NamedResourceController
205
+ end
206
+
207
+ Tip: If you put your module inside the `ContextExposer` namespace, you can even use the `context_exposer` macro ;)
208
+
209
+ ## Integrations with other exposure gems and patterns
210
+
211
+ You can use the class macro `integrate_with(name)` to integrate with either:
212
+
213
+ * decent_exposure - `integrate_with :decent_exposure`
214
+ * decorates_before_rendering - `integrate_with :decorates_before`
215
+ * instance vars - `integrate_with :instance_vars`
216
+
217
+ Note: You can even integrate with multiple strategies
136
218
 
137
- Initial integration support is included for `decent_exposure`, another popular gem with similar functionality. To add methods exposed by `decent_exposure` `#expose` to the context object, simply call `#context_expose_decently` or the alias method `#expose_decently`.
219
+ `integrate_with :decent_exposure, :instance_vars`
220
+
221
+ You can also specify your integrations directly as part of your `context_exposer` call (recommended)
222
+
223
+ `context_exposer :base, with: :decent_exposure`
224
+
225
+ In case you use the usual (default) Rails pattern of passing instance variables, you can slowly migrate to exposing via `ctx` object, by adding a simple macro `context_expose :instance_vars` to your controller.
226
+
227
+ For decorated instance variables (see `decorates_before_rendering` gem), similarly use `context_expose :decorated_instance_vars`.
228
+
229
+ All of these `context_expose :xxxx` methods can optionally take an `:except` or `:only` option with a list of keys, similar to a `before_filter`.
230
+
231
+ The method `context_expose :decorated_instance_vars` can additionally take a `:for`option of either `:collection` or `:non_collection` to limit the type of instance vars exposed.
232
+
233
+ `context_expose` integration
234
+
235
+ * :instance_vars
236
+ * :decorated_instance_vars
237
+ * :decently
238
+
239
+ Here is a full example demonstrating integration with `decent_exposure`.
138
240
 
139
241
  ```ruby
140
242
  # using gem 'decent_exposure'
141
243
  # auto-included in ActionController::Base
142
244
 
143
245
  class PostsController < ActionController::Base
144
- include ContextExposer::ResourceController
246
+ # make context_expose_decently method available
247
+ context_exposer :base, with :decent_exposure
145
248
 
146
- expose(:posts)
147
- expose(:post) { Post.first}
148
- expose(:postal)
249
+ expose(:posts) { Post.all.order(:created_at, :asc) }
250
+ expose(:post) { Post.first}
251
+ expose(:postal) { '1234' }
149
252
 
150
- context_expose_decently except: 'postal'
253
+ # mirror all methods exposed via #expose on #ctx object
254
+ # except for 'postal' method
255
+ context_expose :decently except: 'postal'
151
256
  end
152
257
  ```
153
258
 
@@ -155,10 +260,75 @@ HAML view example
155
260
 
156
261
  ```haml
157
262
  %h1 Posts
158
- = context.posts.each do |post|
263
+ = ctx.posts.each do |post|
159
264
  %h2 = post.name
160
265
  ```
161
266
 
267
+ ## Decorates before rendering
268
+
269
+ A patch for the `decorates_before_render` gem is currently made available.
270
+
271
+ `ContextExposer.patch :decorates_before_rendering`
272
+
273
+ You typically use this in a Rails initializer. This way, `decorates_before_rendering` should try to decorate all your exposed variables before rendering, whether your view context is exposed as instance vars, methods or on the `ctx` object of the view ;)
274
+
275
+ Note: You can now also use the macro `decorates_before_render` to include the `DecoratesBeforeRendering` module.
276
+
277
+ ### Auto-finding a decorator
278
+
279
+ For the patched version of `decorates_before_render` to work, your exposed and cached object must either have a `model_name` method that returns the name of the model name to be used to calculate the decorator name to use, or alternatively (and with higher precedence if present), a `decorator` method that takes the controller (self) as an argument and returns the full name of the decorator to use ;)
280
+
281
+ Example:
282
+
283
+ ```ruby
284
+ class PostsController < ActionController::Base
285
+ decorates_before_render
286
+ context_exposer :base, with :decent_exposure
287
+
288
+ expose_cached(:first_post) { Post.first }
289
+
290
+ protected
291
+
292
+ def admin?
293
+ @admin ||= current_user.admin?
294
+ end
295
+ end
296
+ ```
297
+
298
+ ```ruby
299
+ class Post < ActiveRecord::Base
300
+ def decorator contrl
301
+ contrl.send(:admin?) ? 'Admin::PostDecorator' : model_name
302
+ end
303
+ end
304
+
305
+ ### Auto-detection Error handling
306
+
307
+ If the auto-decoration can't find a decorator for an exposed variable (or method), it will either ignore it (not decorate it) or call `__handle_decorate_error_(error)` which by default will log a Rails warning. Override this error handler as it suits you.
308
+
309
+ ## Testing
310
+
311
+ The tests have been written in rspec 2 and capybara.
312
+ The test suite consists of:
313
+
314
+ * Full app tests
315
+ * Units tests
316
+
317
+ ### Dummy app feature tests
318
+
319
+ A Dummy app has been set up for use with Capybara feature testing.
320
+ Please see: http://alindeman.github.com/2012/11/11/rspec-rails-and-capybara-2.0-what-you-need-to-know.html
321
+
322
+ The feature tests can be found in `spec/app`
323
+
324
+ `$ bundle exec rspec spec/context_exposer`
325
+
326
+ ### Unit tests (specs)
327
+
328
+ The unit tests can be found in `spec/context_exposer`
329
+
330
+ `$ bundle exec rspec spec/context_exposer`
331
+
162
332
  ## Contributing
163
333
 
164
334
  1. Fork it
@@ -1,9 +1,24 @@
1
1
  require "context_exposer/version"
2
2
 
3
3
  module ContextExposer
4
+ def self.patch name
5
+ case name.to_sym
6
+ when :decorates_before_rendering
7
+ require "context_exposer/patch/#{name}"
8
+ else
9
+ raise ArgumentError, "No patch defined for: #{name}. Try one of #{patches}"
10
+ end
11
+ end
12
+
13
+ def self.patches
14
+ [:decorates_before_rendering]
15
+ end
4
16
  end
5
17
 
6
18
  require "active_support"
7
19
  require "context_exposer/base_controller"
8
20
  require "context_exposer/resource_controller"
21
+ require "context_exposer/cached_resource_controller"
9
22
  require "context_exposer/view_context"
23
+ require "context_exposer/macros"
24
+ require "context_exposer/rails_config"
@@ -1,51 +1,51 @@
1
+ require "context_exposer/integrations"
2
+
1
3
  module ContextExposer::BaseController
2
4
  extend ActiveSupport::Concern
5
+ include ContextExposer::Integrations::Base
3
6
 
4
7
  included do
5
- before_filter :configure_exposed_context
6
-
7
- expose_context :context
8
+ # before_filter :configure_exposed_context
9
+ set_callback :process_action, :before, :configure_exposed_context
8
10
 
9
- # set_callback :process_action, :before, :configure_exposed_context
11
+ expose_context :ctx
10
12
  end
11
13
 
12
- def view_context
13
- @view_context ||= build_view_context
14
+ def view_ctx
15
+ @view_ctx ||= build_view_ctx
14
16
  end
15
- alias_method :context, :view_context
17
+ alias_method :ctx, :view_ctx
16
18
 
17
19
  module ClassMethods
18
- def exposed name, &block
19
- # puts "store: #{name} in hash storage for class #{self}"
20
- exposure_storage[name.to_sym] = block
20
+ def exposed name, options = {}, &block
21
+ _exposure_storage[name.to_sym] = {options: options, proc: block}
21
22
  end
22
23
 
23
- # expose all exposures exposed by decent_exposure to context
24
- def context_expose_decently options = {}
25
- transfer_keys = _exposures.keys
26
- except = (options[:except] || {}).map(&:to_sym)
27
- only = (options[:only] || {}).map(&:to_sym)
28
-
29
- transfer_keys = transfer_keys - except
24
+ def expose_cached name, options = {}, &block
25
+ exposed name, options.merge(cached: true), &block
26
+ end
30
27
 
31
- unless only.empty?
32
- transfer_keys.select {|k| only.include? k.to_sym }
28
+ def view_ctx_class name
29
+ define_method :view_ctx_class do
30
+ @view_ctx_class ||= name.kind_of?(Class) ? name : name.to_s.camelize.constantize
33
31
  end
32
+ end
34
33
 
35
- transfer_keys.each do |exposure|
36
- exposed exposure do
37
- send(exposure)
38
- end
34
+ def integrate_with *names
35
+ names.flatten.compact.each do |name|
36
+ self.send :include, "ContextExposer::Integrations::With#{name.to_s.camelize}".constantize
39
37
  end
40
38
  end
41
- alias_method :expose_decently, :context_expose_decently
39
+ alias_method :integrates_with, :integrate_with
42
40
 
43
- def view_context_class name
44
- define_method :view_context_class do
45
- @view_context_class ||= name.kind_of?(Class) ? name : name.to_s.camelize.constantize
46
- end
41
+ def context_expose name, options = {}
42
+ send "context_expose_#{name}", options
47
43
  end
48
44
 
45
+ def _exposure_storage
46
+ _exposure_hash[self.to_s] ||= {}
47
+ end
48
+
49
49
  protected
50
50
 
51
51
  def expose_context name
@@ -59,32 +59,26 @@ module ContextExposer::BaseController
59
59
  end
60
60
  helper_method name
61
61
  hide_action name
62
- @exposed_view_context = true
62
+ @_exposed_view_context = true
63
63
  end
64
64
 
65
65
  def exposed_view_context?
66
- @exposed_view_context == true
66
+ @_exposed_view_context == true
67
67
  end
68
68
 
69
- def exposure_storage
70
- exposure_hash[self.to_s] ||= {}
71
- end
72
-
73
- def exposure_hash
74
- @exposure_hash ||= {}
75
- end
69
+ def _exposure_hash
70
+ @_exposure_hash ||= {}
71
+ end
76
72
  end
77
73
 
78
74
  # must be called after Controller is instantiated
79
75
  def configure_exposed_context
80
76
  return if configured_exposed_context?
81
77
  clazz = self.class
82
- exposed_methods = clazz.send(:exposure_hash)[clazz.to_s] || []
83
- exposed_methods.each do |name, procedure|
84
- this = self
85
- view_context.send :define_singleton_method, name do
86
- this.instance_eval(&procedure)
87
- end
78
+ exposed_methods = clazz.send(:_exposure_hash)[clazz.to_s] || []
79
+ exposed_methods.each do |name, obj|
80
+ options = obj[:options] || {}
81
+ options[:cached] ? _add_cached_ctx_method(obj, name) : _add_ctx_method(obj, name)
88
82
  end
89
83
  @configured_exposed_context = true
90
84
  end
@@ -95,13 +89,39 @@ module ContextExposer::BaseController
95
89
 
96
90
  protected
97
91
 
92
+ def _add_ctx_method obj, name
93
+ this = self
94
+ proc = obj[:proc]
95
+ inst_var_name = "@#{name}"
96
+
97
+ view_ctx.send :define_singleton_method, name do
98
+ this.instance_eval(&proc)
99
+ end
100
+ end
101
+
102
+ def _add_cached_ctx_method obj, name
103
+ this = self
104
+ options = obj[:options]
105
+ proc = obj[:proc]
106
+ inst_var_name = "@#{name}"
107
+
108
+ view_ctx.send :define_singleton_method, name do
109
+ old_val = instance_variable_get inst_var_name
110
+ return old_val if old_val
111
+
112
+ val = this.instance_eval(&proc)
113
+ instance_variable_set inst_var_name, val
114
+ val
115
+ end
116
+ end
117
+
98
118
  # returns a ViewContext object
99
119
  # view helpers can be exposed as singleton methods, dynamically be attached (see below)
100
- def build_view_context
101
- view_context_class.new self
120
+ def build_view_ctx
121
+ view_ctx_class.new self
102
122
  end
103
123
 
104
- def view_context_class
105
- @view_context_class ||= ContextExposer::ViewContext
124
+ def view_ctx_class
125
+ @view_ctx_class ||= ContextExposer::ViewContext
106
126
  end
107
127
  end