decent_exposure 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE html PUBLIC
3
+ "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN"
4
+ "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">
5
+ <html xmlns:svg='http://www.w3.org/2000/svg' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'>
6
+ <head><meta content='application/xhtml+xml;charset=utf-8' http-equiv='Content-type' /><title>DecentExposure</title></head>
7
+ <body>
8
+ <h1 id='decentexposure'>DecentExposure</h1>
9
+
10
+ <p><em>Copying over instance variables is bad, mmm-kay?</em></p>
11
+
12
+ <p>DecentExposure helps you program to an interface, rather than an implementation in your Rails controllers.</p>
13
+
14
+ <p>Sharing state via instance variables in controllers promotes close coupling with views. DecentExposure gives you a declarative manner of exposing an interface to the state that controllers contain, thereby decreasing coupling and improving your testability and overall design.</p>
15
+
16
+ <h2 id='installation'>Installation</h2>
17
+
18
+ <pre><code>gem install decent_exposure</code></pre>
19
+
20
+ <p>Configure your application to use it:</p>
21
+
22
+ <p>In <code>config/environment.rb</code>:</p>
23
+
24
+ <pre><code>config.gem &#39;decent_exposure&#39;</code></pre>
25
+
26
+ <h2 id='the_particulars'>The Particulars</h2>
27
+
28
+ <p><code>expose</code> creates a method with the given name, evaluates the provided block (or intuits a value when no block is passed) and memoizes the result. This method is then declared as a <code>helper_method</code> so that views may have access to it and is made unroutable as an action.</p>
29
+
30
+ <h2 id='examples'>Examples</h2>
31
+
32
+ <h3 id='in_your_controllers'>In your controllers</h3>
33
+
34
+ <p>When no block is given, <code>expose</code> attempts to intuit which resource you want to acquire:</p>
35
+
36
+ <pre><code># Category.find(params[:category_id] || params[:id])
37
+ expose(:category)</code></pre>
38
+
39
+ <p>As the example shows, the symbol passed is used to guess the class name of the object you want an instance of. Almost every controller has one of these. In the RESTful controller paradigm, you might use this in <code>#show</code>, <code>#edit</code>, <code>#update</code> or <code>#destroy</code>.</p>
40
+
41
+ <p>In the slightly more complicated scenario, you need to find an instance of an object which doesn&#8217;t map cleanly to <code>Object#find</code>:</p>
42
+
43
+ <pre><code>expose(:product){ category.products.find(params[:id]) }</code></pre>
44
+
45
+ <p>In the RESTful controller paradigm, you&#8217;ll again find yourself using this in <code>#show</code>, <code>#edit</code>, <code>#update</code> or <code>#destroy</code>.</p>
46
+
47
+ <p>When the code has become complex enough to surpass a single line (and is not appropriate to extract into a model method), use the <code>do...end</code> style of block:</p>
48
+
49
+ <pre><code>expose(:associated_products) do
50
+ product.associated.tap do |associated_products|
51
+ present(associated_products, :with =&gt; AssociatedProductPresenter)
52
+ end
53
+ end</code></pre>
54
+
55
+ <h3 id='in_your_views'>In your views</h3>
56
+
57
+ <p>Use the product of those assignments like you would an instance variable or any other method you might normally have access to:</p>
58
+
59
+ <pre><code>= render bread_crumbs_for(category)
60
+ %h3#product_title= product.title
61
+ = render product
62
+ %h3 Associated Products
63
+ %ul
64
+ - associated_products.each do |associated_product|
65
+ %li= link_to(associated_product.title,product_path(associated_product))</code></pre>
66
+
67
+ <h3 id='custom_defaults'>Custom defaults</h3>
68
+
69
+ <p>DecentExposure provides opinionated default logic when <code>expose</code> is invoked without a block. It&#8217;s possible, however, to override this with your own custom default logic by passing a block accepting a single argument to the <code>default_exposure</code> method inside of a controller. The argument will be the string or symbol passed in to the <code>expose</code> call.</p>
70
+
71
+ <pre><code>class MyController &lt; ApplicationController
72
+ default_exposure do |name|
73
+ ObjectCache.load(name.to_s)
74
+ end
75
+ end</code></pre>
76
+
77
+ <p>The given block will be invoked in the context of a controller instance. It is possible to provide a custom default for a descendant class without disturbing its ancestor classes in an inheritance heirachy.</p>
78
+
79
+ <p><strong>Caveat</strong>: Note that the simplest way to provide custom default <code>expose</code> logic for all of your controllers is to invoke <code>default_exposure</code> inside of <code>ApplicationController</code>. Due to the order of Rails&#8217; initialization logic, na&#239;ve attempts to invoke it in <code>ActionController::Base</code> will have no affect. Use an initializer if you need this behavior.</p>
80
+
81
+ <h2 id='beware'>Beware</h2>
82
+
83
+ <p>This is an exceptionally simple tool, which provides a solitary solution. It must be used in conjunction with solid design approaches (&#8220;Program to an interface, not an implementation.&#8221;) and accepted best practices (e.g. Fat Model, Skinny Controller). In itself, it won&#8217;t heal a bad design. It is meant only to be a tool to use in improving the overall design of a Ruby on Rails system and moreover to provide a standard implementation for an emerging best practice.</p>
84
+
85
+ <h2 id='development'>Development</h2>
86
+
87
+ <h3 id='running_specs'>Running specs</h3>
88
+
89
+ <p><code>DecentExposure</code> has been developed with the philosophy that Ruby developers shouldn&#8217;t force their choice in RubyGems package managers on people consuming their code. As a side effect of that, if you attempt to run the specs on this application, you might get <code>no such file to load</code> errors. The short answer is that you can <code>export RUBYOPT=&#39;rubygems&#39;</code> and be on about your way (for the long answer, see Ryan Tomayko&#8217;s <a href='http://tomayko.com/writings/require-rubygems-antipattern'>excellent treatise</a> on the subject).</p>
90
+
91
+ <h3 id='documentation_todo'>Documentation TODO</h3>
92
+
93
+ <ul>
94
+ <li>walk-through of an actual implementation (using an existing, popular OSS Rails app as an example refactor).</li>
95
+ </ul>
96
+ </body></html>
data/README.md CHANGED
@@ -76,6 +76,30 @@ other method you might normally have access to:
76
76
  - associated_products.each do |associated_product|
77
77
  %li= link_to(associated_product.title,product_path(associated_product))
78
78
 
79
+ ### Custom defaults
80
+
81
+ DecentExposure provides opinionated default logic when `expose` is invoked without
82
+ a block. It's possible, however, to override this with your own custom default
83
+ logic by passing a block accepting a single argument to the `default_exposure`
84
+ method inside of a controller. The argument will be the string or symbol passed
85
+ in to the `expose` call.
86
+
87
+ class MyController < ApplicationController
88
+ default_exposure do |name|
89
+ ObjectCache.load(name.to_s)
90
+ end
91
+ end
92
+
93
+ The given block will be invoked in the context of a controller instance. It is
94
+ possible to provide a custom default for a descendant class without disturbing
95
+ its ancestor classes in an inheritance heirachy.
96
+
97
+ **Caveat**: Note that the simplest way to provide custom default `expose` logic
98
+ for all of your controllers is to invoke `default_exposure` inside of
99
+ `ApplicationController`. Due to the order of Rails' initialization logic,
100
+ attempts to invoke it in `ActionController::Base` will have no affect. Use an
101
+ initializer if you need this behavior.
102
+
79
103
  Beware
80
104
  ------
81
105
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -1,21 +1,28 @@
1
1
  module DecentExposure
2
+ def inherited(klass)
3
+ closured_exposure = default_exposure
4
+ klass.class_eval do
5
+ default_exposure(&closured_exposure)
6
+ end
7
+ end
8
+
9
+ def default_exposure(&block)
10
+ @default_exposure = block if block_given?
11
+ @default_exposure
12
+ end
13
+
2
14
  def expose(name, &block)
15
+ closured_exposure = default_exposure
3
16
  define_method name do
4
17
  @_resources ||= {}
5
18
  @_resources[name] ||= if block_given?
6
19
  instance_eval(&block)
7
20
  else
8
- _class_for(name).find(params["#{name}_id"] || params['id'])
21
+ instance_exec(name, &closured_exposure)
9
22
  end
10
23
  end
11
24
  helper_method name
12
25
  hide_action name
13
26
  end
14
-
15
27
  alias let expose
16
-
17
- private
18
- def _class_for(name)
19
- name.to_s.classify.constantize
20
- end
21
28
  end
@@ -4,4 +4,10 @@ rescue LoadError
4
4
  require 'decent_exposure' # From gem
5
5
  end
6
6
 
7
- ActionController::Base.extend(DecentExposure)
7
+ ActionController::Base.class_eval do
8
+ extend DecentExposure
9
+ default_exposure do |name|
10
+ model_class = name.to_s.classify.constantize
11
+ model_class.find(params["#{name}_id"] || params['id'])
12
+ end
13
+ end
@@ -4,11 +4,28 @@ class Quacker
4
4
  extend DecentExposure
5
5
  def self.helper_method(*args); end
6
6
  def self.hide_action(*args); end
7
- def self.find(*args); end
8
7
  def memoizable(*args); args; end
9
- def params; {'proxy_id' => 42}; end
10
8
  expose(:proxy)
11
- expose(:quack){ memoizable('quack!') }
9
+ end
10
+
11
+ module ActionController
12
+ class Base
13
+ def self.helper_method(*args); end
14
+ def self.hide_action(*args); end
15
+ def params; {'resource_id' => 42}; end
16
+ end
17
+ end
18
+ require File.join(File.dirname(__FILE__), '..', '..', 'rails', 'init.rb')
19
+
20
+ class MyController < ActionController::Base
21
+ end
22
+
23
+ class Resource
24
+ def self.find(*args); end
25
+ end
26
+
27
+ class Widget
28
+ def self.find(*args); end
12
29
  end
13
30
 
14
31
  describe DecentExposure do
@@ -22,7 +39,7 @@ describe DecentExposure do
22
39
  let(:instance){ Quacker.new }
23
40
 
24
41
  it "creates a method with the given name" do
25
- Quacker.new.methods.map{|m| m.to_s}.should include('quack')
42
+ Quacker.new.methods.map{|m| m.to_s}.should include('proxy')
26
43
  end
27
44
 
28
45
  it "prevents the method from being a callable action" do
@@ -40,7 +57,12 @@ describe DecentExposure do
40
57
  end
41
58
  end
42
59
 
43
- it "returns the value of the method" do
60
+ it "returns the result of the exposed block from the method" do
61
+ Quacker.stubs(:hide_action)
62
+ Quacker.stubs(:helper_method)
63
+ Quacker.class_eval do
64
+ expose(:quack){ memoizable('quack!') }
65
+ end
44
66
  instance.quack.should == %w(quack!)
45
67
  end
46
68
 
@@ -50,46 +72,103 @@ describe DecentExposure do
50
72
  instance.quack
51
73
  end
52
74
 
53
- context "when no block is given" do
54
- before do
55
- instance.stubs(:_class_for).returns(Quacker)
75
+ context "when specifying custom default behavior" do
76
+ before(:all) do
77
+ class ::DuckController
78
+ extend DecentExposure
79
+ def self.helper_method(*args); end
80
+ def self.hide_action(*args); end
81
+ def params; end
82
+ default_exposure {"default value"}
83
+ expose(:quack)
84
+ end
85
+ end
86
+
87
+ it "uses that behavior when no block is given" do
88
+ DuckController.new.quack.should == "default value"
56
89
  end
57
- it "attempts to guess the class of the resource to expose" do
58
- instance.expects(:_class_for).with(:proxy).returns(Quacker)
59
- instance.proxy
90
+
91
+ it "passes the name given to #expose into the block" do
92
+ DuckController.class_eval do
93
+ default_exposure {|name| "downy #{name}"}
94
+ expose :feathers
95
+ end
96
+ DuckController.new.feathers.should == "downy feathers"
60
97
  end
61
- it "calls find with {resource}_id on the resources class" do
62
- Quacker.expects(:find).with(42)
63
- instance.proxy
98
+ end
99
+ end
100
+
101
+ context "within Rails" do
102
+ let(:controller) {ActionController::Base.new}
103
+
104
+ let(:resource){ 'resource' }
105
+ let(:resource_class_name){ 'Resource' }
106
+ before do
107
+ resource.stubs(:to_s => resource, :classify => resource_class_name)
108
+ resource_class_name.stubs(:constantize => Resource)
109
+ end
110
+
111
+ it "extends ActionController::Base" do
112
+ ActionController::Base.respond_to?(:expose).should == true
113
+ end
114
+
115
+ context "by default" do
116
+ it "calls find with params[:resource_id] on the resource's class" do
117
+ name = resource
118
+ Resource.expects(:find).with(42)
119
+ ActionController::Base.class_eval do
120
+ expose name
121
+ end
122
+ controller.resource
64
123
  end
65
- context "and there is no {resource}_id" do
124
+ context "or, when there is no :resource_id in params" do
66
125
  before do
67
- Quacker.class_eval do
126
+ ActionController::Base.class_eval do
68
127
  def params; {'id' => 24}; end
69
128
  end
70
129
  end
71
- it "calls find with params[:id] on the resources class" do
72
- Quacker.expects(:find).with(24)
73
- instance.proxy
130
+ it "calls find with params[:id] on the resource's class" do
131
+ Resource.expects(:find).with(24)
132
+ controller.resource
74
133
  end
75
134
  end
76
135
  end
77
- end
78
136
 
79
- describe '#_class_for' do
80
- let(:name){ 'quacker' }
81
- let(:classified_name){ 'Quacker' }
137
+ let(:widget){ 'widget' }
138
+ let(:widget_class_name){ 'Widget' }
82
139
  before do
83
- name.stubs(:to_s => name, :classify => classified_name)
84
- classified_name.stubs(:constantize => Quacker)
140
+ widget.stubs(:to_s => widget, :classify => widget_class_name)
141
+ widget_class_name.stubs(:constantize => Widget)
142
+ end
143
+
144
+ let(:my_controller) {MyController.new}
145
+
146
+ it "works in descendant controllers" do
147
+ name = widget
148
+ Widget.expects(:find).with(123).returns('a widget')
149
+ MyController.class_eval do
150
+ def params; {'id' => 123} end
151
+ expose name
152
+ end
153
+
154
+ my_controller.widget.should == 'a widget'
85
155
  end
86
- it 'retrieves a string representation of the class name' do
87
- name.expects(:classify).returns(classified_name)
88
- Quacker.send(:_class_for,name)
156
+
157
+ it "allows overridden default in descendant controllers" do
158
+ MyController.class_eval do
159
+ default_exposure {|name| name.to_s}
160
+ expose :overridden
161
+ end
162
+ my_controller.overridden.should == 'overridden'
89
163
  end
90
- it 'returns the string representation of the name as a constant' do
91
- classified_name.expects(:constantize)
92
- Quacker.send(:_class_for,name)
164
+
165
+ it "preserves default in ancestors" do
166
+ name = widget
167
+ Widget.stubs(:find).returns('preserved')
168
+ ActionController::Base.class_eval do
169
+ expose name
170
+ end
171
+ controller.widget.should == 'preserved'
93
172
  end
94
173
  end
95
174
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decent_exposure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Caudill
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2010-01-31 00:00:00 -05:00
13
+ date: 2010-02-19 00:00:00 -05:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -23,6 +23,16 @@ dependencies:
23
23
  - !ruby/object:Gem::Version
24
24
  version: 1.2.9
25
25
  version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ type: :development
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.9.8
35
+ version:
26
36
  description: "\n DecentExposure helps you program to an interface, rather than an implementation\n in your Rails controllers. The fact of the matter is that sharing state\n via instance variables in controllers promotes close coupling with views.\n DecentExposure gives you a declarative manner of exposing an interface to the\n state that controllers contain and thereby decreasing coupling and\n improving your testability and overall design.\n "
27
37
  email: scaudill@gmail.com
28
38
  executables: []
@@ -30,6 +40,7 @@ executables: []
30
40
  extensions: []
31
41
 
32
42
  extra_rdoc_files:
43
+ - README.html
33
44
  - README.md
34
45
  files:
35
46
  - COPYING
@@ -37,6 +48,7 @@ files:
37
48
  - VERSION
38
49
  - lib/decent_exposure.rb
39
50
  - rails/init.rb
51
+ - README.html
40
52
  has_rdoc: true
41
53
  homepage: http://github.com/voxdolo/decent_exposure
42
54
  licenses: []