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.
- data/README.html +96 -0
- data/README.md +24 -0
- data/VERSION +1 -1
- data/lib/decent_exposure.rb +14 -7
- data/rails/init.rb +7 -1
- data/spec/lib/decent_exposure_spec.rb +110 -31
- metadata +14 -2
data/README.html
ADDED
@@ -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 'decent_exposure'</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’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’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 => 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’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 < 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’ initialization logic, naï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 (“Program to an interface, not an implementation.”) and accepted best practices (e.g. Fat Model, Skinny Controller). In itself, it won’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’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='rubygems'</code> and be on about your way (for the long answer, see Ryan Tomayko’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.
|
1
|
+
0.2.1
|
data/lib/decent_exposure.rb
CHANGED
@@ -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
|
-
|
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
|
data/rails/init.rb
CHANGED
@@ -4,4 +4,10 @@ rescue LoadError
|
|
4
4
|
require 'decent_exposure' # From gem
|
5
5
|
end
|
6
6
|
|
7
|
-
ActionController::Base.
|
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
|
-
|
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('
|
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
|
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
|
54
|
-
before do
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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 "
|
124
|
+
context "or, when there is no :resource_id in params" do
|
66
125
|
before do
|
67
|
-
|
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
|
72
|
-
|
73
|
-
|
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
|
-
|
80
|
-
let(:
|
81
|
-
let(:classified_name){ 'Quacker' }
|
137
|
+
let(:widget){ 'widget' }
|
138
|
+
let(:widget_class_name){ 'Widget' }
|
82
139
|
before do
|
83
|
-
|
84
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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.
|
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-
|
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: []
|