context_exposer 0.1.0 → 0.3.0

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.
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