decent_exposure 1.0.0.rc1 → 1.0.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.html +119 -0
- data/README.md +62 -45
- data/VERSION +1 -1
- data/lib/decent_exposure/default_exposure.rb +9 -27
- data/spec/lib/decent_exposure_spec.rb +1 -1
- data/spec/lib/rails_integration_spec.rb +27 -2
- metadata +17 -7
data/README.html
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<style type='text/css'>
|
4
|
+
body{ width:50em; }
|
5
|
+
pre{ margin-left:2em; }
|
6
|
+
</style>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<h1 id='decentexposure'>DecentExposure</h1>
|
10
|
+
|
11
|
+
<p>DecentExposure helps you program to an interface, rather than an implementation in your Rails controllers.</p>
|
12
|
+
|
13
|
+
<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>
|
14
|
+
|
15
|
+
<h2 id='installation'>Installation</h2>
|
16
|
+
|
17
|
+
<pre><code>gem install decent_exposure</code></pre>
|
18
|
+
|
19
|
+
<p>Configure your Rails 2.X application to use it:</p>
|
20
|
+
|
21
|
+
<p>In <code>config/environment.rb</code>:</p>
|
22
|
+
|
23
|
+
<pre><code>config.gem 'decent_exposure'</code></pre>
|
24
|
+
|
25
|
+
<p>When used in Rails 3:</p>
|
26
|
+
|
27
|
+
<p>In <code>Gemfile</code>:</p>
|
28
|
+
|
29
|
+
<pre><code>gem 'decent_exposure'</code></pre>
|
30
|
+
|
31
|
+
<h2 id='examples'>Examples</h2>
|
32
|
+
|
33
|
+
<h3 id='a_full_example'>A full example</h3>
|
34
|
+
|
35
|
+
<p>The wiki has a full example of <a href='http://github.com/voxdolo/decent_exposure/wiki/Examples'>converting a classic-style Rails controller</a>.</p>
|
36
|
+
|
37
|
+
<h3 id='in_your_controllers'>In your controllers</h3>
|
38
|
+
|
39
|
+
<p>When no block is given, <code>expose</code> attempts to determine which resource you want to acquire. When <code>params</code> contains <code>:category_id</code> or <code>:id</code>, a call to:</p>
|
40
|
+
|
41
|
+
<pre><code>expose(:category)</code></pre>
|
42
|
+
|
43
|
+
<p>Would result in the following <code>ActiveRecord#find</code>:</p>
|
44
|
+
|
45
|
+
<pre><code>Category.find(params[:category_id]||params[:id])</code></pre>
|
46
|
+
|
47
|
+
<p>As the example shows, the symbol passed is used to guess the class name of the object (and potentially the <code>params</code> key to find it with) you want an instance of.</p>
|
48
|
+
|
49
|
+
<p>Should <code>params</code> not contain an identifiable <code>id</code>, a call to:</p>
|
50
|
+
|
51
|
+
<pre><code>expose(:category)</code></pre>
|
52
|
+
|
53
|
+
<p>Will instead attempt to build a new instance of the object like so:</p>
|
54
|
+
|
55
|
+
<pre><code>Category.new(params[:category])</code></pre>
|
56
|
+
|
57
|
+
<p>If you define a collection with a pluralized name of the singular resource, <code>decent_exposure</code> will attempt to use it to scope its calls from. Let’s take the following scenario:</p>
|
58
|
+
|
59
|
+
<pre><code>class ProductsController < ApplicationController
|
60
|
+
expose(:category)
|
61
|
+
expose(:products) { category.products }
|
62
|
+
expose(:product)
|
63
|
+
end</code></pre>
|
64
|
+
|
65
|
+
<p>The <code>product</code> resource would scope from the <code>products</code> collection via a fully-expanded query equivalent to this:</p>
|
66
|
+
|
67
|
+
<pre><code>Category.find(params[:category_id]).products.find(params[:id])</code></pre>
|
68
|
+
|
69
|
+
<p>or (depending on the contents of the <code>params</code> hash) this:</p>
|
70
|
+
|
71
|
+
<pre><code>Category.find(params[:category_id]).products.new(params[:product])</code></pre>
|
72
|
+
|
73
|
+
<p>In the straightforward case, the three exposed resources above provide for access to both the primary and ancestor resources in a way usable across all 7 actions in a typicall Rails-style RESTful controller.</p>
|
74
|
+
|
75
|
+
<h4 id='a_note_on_style'>A Note on Style</h4>
|
76
|
+
|
77
|
+
<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>
|
78
|
+
|
79
|
+
<pre><code>expose(:associated_products) do
|
80
|
+
product.associated.tap do |associated_products|
|
81
|
+
present(associated_products, :with => AssociatedProductPresenter)
|
82
|
+
end
|
83
|
+
end</code></pre>
|
84
|
+
|
85
|
+
<h3 id='in_your_views'>In your views</h3>
|
86
|
+
|
87
|
+
<p>Use the product of those assignments like you would an instance variable or any other method you might normally have access to:</p>
|
88
|
+
|
89
|
+
<pre><code>= render bread_crumbs_for(category)
|
90
|
+
%h3#product_title= product.title
|
91
|
+
= render product
|
92
|
+
%h3 Associated Products
|
93
|
+
%ul
|
94
|
+
- associated_products.each do |associated_product|
|
95
|
+
%li= link_to(associated_product.title,product_path(associated_product))</code></pre>
|
96
|
+
|
97
|
+
<h3 id='custom_defaults'>Custom defaults</h3>
|
98
|
+
|
99
|
+
<p>DecentExposure provides opinionated default logic when <code>expose</code> is invoked without a block. It’s possible, however, to override this with 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>
|
100
|
+
|
101
|
+
<pre><code>class MyController < ApplicationController
|
102
|
+
default_exposure do |name|
|
103
|
+
ObjectCache.load(name.to_s)
|
104
|
+
end
|
105
|
+
end</code></pre>
|
106
|
+
|
107
|
+
<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>
|
108
|
+
|
109
|
+
<h2 id='beware'>Beware</h2>
|
110
|
+
|
111
|
+
<p>This is a 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>
|
112
|
+
|
113
|
+
<h2 id='development'>Development</h2>
|
114
|
+
|
115
|
+
<h3 id='running_specs'>Running specs</h3>
|
116
|
+
|
117
|
+
<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>
|
118
|
+
</body>
|
119
|
+
</html>
|
data/README.md
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
|
2
|
-
==============
|
3
|
-
|
4
|
-
_Copying over instance variables is bad, mmm-kay?_
|
5
|
-
|
6
|
-
DecentExposure helps you program to an interface, rather than an implementation in
|
1
|
+
`decent_exposure` helps you program to an interface, rather than an implementation in
|
7
2
|
your Rails controllers.
|
8
3
|
|
9
4
|
Sharing state via instance variables in controllers promotes close coupling with
|
10
|
-
views.
|
5
|
+
views. `decent_exposure` gives you a declarative manner of exposing an interface to the
|
11
6
|
state that controllers contain, thereby decreasing coupling and improving your
|
12
|
-
testability and overall design.
|
7
|
+
testability and overall design. I elaborate on this approach in [A Diatribe on
|
8
|
+
Maintaining State][diatribe].
|
13
9
|
|
14
10
|
Installation
|
15
11
|
------------
|
@@ -29,37 +25,61 @@ In `Gemfile`:
|
|
29
25
|
gem 'decent_exposure'
|
30
26
|
|
31
27
|
|
32
|
-
The Particulars
|
33
|
-
---------------
|
34
|
-
|
35
|
-
`expose` creates a method with the given name, evaluates the provided block (or
|
36
|
-
intuits a value when no block is passed) and memoizes the result. This method is
|
37
|
-
then declared as a `helper_method` so that views may have access to it and is
|
38
|
-
made unroutable as an action.
|
39
|
-
|
40
28
|
Examples
|
41
29
|
--------
|
42
30
|
|
31
|
+
### A full example
|
32
|
+
|
33
|
+
The wiki has a full example of [converting a classic-style Rails
|
34
|
+
controller][converting].
|
35
|
+
|
43
36
|
### In your controllers
|
44
37
|
|
45
|
-
When no block is given, `expose` attempts to
|
46
|
-
acquire:
|
38
|
+
When no block is given, `expose` attempts to determine which resource you want
|
39
|
+
to acquire. When `params` contains `:category_id` or `:id`, a call to:
|
47
40
|
|
48
|
-
# Category.find(params[:category_id] || params[:id])
|
49
41
|
expose(:category)
|
50
42
|
|
43
|
+
Would result in the following `ActiveRecord#find`:
|
44
|
+
|
45
|
+
Category.find(params[:category_id]||params[:id])
|
46
|
+
|
51
47
|
As the example shows, the symbol passed is used to guess the class name of the
|
52
|
-
object
|
53
|
-
|
54
|
-
|
48
|
+
object (and potentially the `params` key to find it with) you want an instance
|
49
|
+
of.
|
50
|
+
|
51
|
+
Should `params` not contain an identifiable `id`, a call to:
|
52
|
+
|
53
|
+
expose(:category)
|
54
|
+
|
55
|
+
Will instead attempt to build a new instance of the object like so:
|
56
|
+
|
57
|
+
Category.new(params[:category])
|
58
|
+
|
59
|
+
If you define a collection with a pluralized name of the singular resource,
|
60
|
+
`decent_exposure` will attempt to use it to scope its calls from. Let's take the
|
61
|
+
following scenario:
|
62
|
+
|
63
|
+
class ProductsController < ApplicationController
|
64
|
+
expose(:category)
|
65
|
+
expose(:products) { category.products }
|
66
|
+
expose(:product)
|
67
|
+
end
|
68
|
+
|
69
|
+
The `product` resource would scope from the `products` collection via a
|
70
|
+
fully-expanded query equivalent to this:
|
71
|
+
|
72
|
+
Category.find(params[:category_id]).products.find(params[:id])
|
55
73
|
|
56
|
-
|
57
|
-
object which doesn't map cleanly to `Object#find`:
|
74
|
+
or (depending on the contents of the `params` hash) this:
|
58
75
|
|
59
|
-
|
76
|
+
Category.find(params[:category_id]).products.new(params[:product])
|
60
77
|
|
61
|
-
In the
|
62
|
-
|
78
|
+
In the straightforward case, the three exposed resources above provide for
|
79
|
+
access to both the primary and ancestor resources in a way usable across all 7
|
80
|
+
actions in a typicall Rails-style RESTful controller.
|
81
|
+
|
82
|
+
#### A Note on Style
|
63
83
|
|
64
84
|
When the code has become complex enough to surpass a single line (and is not
|
65
85
|
appropriate to extract into a model method), use the `do...end` style of block:
|
@@ -85,11 +105,11 @@ other method you might normally have access to:
|
|
85
105
|
|
86
106
|
### Custom defaults
|
87
107
|
|
88
|
-
|
89
|
-
a block. It's possible, however, to override this with
|
90
|
-
|
91
|
-
|
92
|
-
|
108
|
+
`decent_exposure` provides opinionated default logic when `expose` is invoked without
|
109
|
+
a block. It's possible, however, to override this with custom default logic by
|
110
|
+
passing a block accepting a single argument to the `default_exposure` method
|
111
|
+
inside of a controller. The argument will be the string or symbol passed in to
|
112
|
+
the `expose` call.
|
93
113
|
|
94
114
|
class MyController < ApplicationController
|
95
115
|
default_exposure do |name|
|
@@ -104,11 +124,11 @@ its ancestor classes in an inheritance heirachy.
|
|
104
124
|
Beware
|
105
125
|
------
|
106
126
|
|
107
|
-
This is
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
127
|
+
This is a simple tool, which provides a solitary solution. It must be used in
|
128
|
+
conjunction with solid design approaches ("Program to an interface, not an
|
129
|
+
implementation.") and accepted best practices (e.g. Fat Model, Skinny
|
130
|
+
Controller). In itself, it won't heal a bad design. It is meant only to be a
|
131
|
+
tool to use in improving the overall design of a Ruby on Rails system and
|
112
132
|
moreover to provide a standard implementation for an emerging best practice.
|
113
133
|
|
114
134
|
Development
|
@@ -116,16 +136,13 @@ Development
|
|
116
136
|
|
117
137
|
### Running specs
|
118
138
|
|
119
|
-
`
|
139
|
+
`decent_exposure` has been developed with the philosophy that Ruby developers shouldn't
|
120
140
|
force their choice in RubyGems package managers on people consuming their code.
|
121
141
|
As a side effect of that, if you attempt to run the specs on this application,
|
122
142
|
you might get `no such file to load` errors. The short answer is that you can
|
123
143
|
`export RUBYOPT='rubygems'` and be on about your way (for the long answer, see
|
124
|
-
Ryan Tomayko's [excellent
|
125
|
-
treatise](http://tomayko.com/writings/require-rubygems-antipattern) on the
|
126
|
-
subject).
|
127
|
-
|
128
|
-
### Documentation TODO
|
144
|
+
Ryan Tomayko's [excellent treatise][treatise] on the subject).
|
129
145
|
|
130
|
-
|
131
|
-
|
146
|
+
[treatise]: http://tomayko.com/writings/require-rubygems-antipattern
|
147
|
+
[converting]: http://github.com/voxdolo/decent_exposure/wiki/Examples
|
148
|
+
[diatribe]: http://blog.voxdolo.me/a-diatribe-on-maintaining-state.html
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.0.
|
1
|
+
1.0.0.rc2
|
@@ -4,39 +4,21 @@ module DecentExposure
|
|
4
4
|
klass.extend(DecentExposure)
|
5
5
|
klass.superclass_delegating_accessor(:_default_exposure)
|
6
6
|
klass.default_exposure do |name|
|
7
|
-
|
7
|
+
collection = name.to_s.pluralize
|
8
|
+
if respond_to?(collection) && collection != name.to_s && send(collection).respond_to?(:scoped)
|
9
|
+
proxy = send(collection)
|
10
|
+
else
|
11
|
+
proxy = name.to_s.classify.constantize
|
12
|
+
end
|
13
|
+
|
8
14
|
if id = params["#{name}_id"] || params[:id]
|
9
|
-
|
15
|
+
proxy.find(id).tap do |r|
|
10
16
|
r.attributes = params[name] unless request.get?
|
11
17
|
end
|
12
18
|
else
|
13
|
-
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
attr_accessor :_resource_name
|
20
|
-
|
21
|
-
def _resource_class
|
22
|
-
_resource_name.classify.constantize
|
23
|
-
end
|
24
|
-
|
25
|
-
def _collection_name
|
26
|
-
_resource_name.pluralize
|
27
|
-
end
|
28
|
-
|
29
|
-
def _proxy
|
30
|
-
_collection.respond_to?(:scoped) ? _collection : _resource_class
|
31
|
-
end
|
32
|
-
|
33
|
-
def _collection
|
34
|
-
unless self.class.method_defined?(_collection_name)
|
35
|
-
self.class.expose(_collection_name) do
|
36
|
-
_collection_name.classify.constantize.scoped({})
|
19
|
+
proxy.new(params[name])
|
37
20
|
end
|
38
21
|
end
|
39
|
-
send(_collection_name)
|
40
22
|
end
|
41
23
|
end
|
42
24
|
end
|
@@ -20,7 +20,7 @@ describe DecentExposure do
|
|
20
20
|
let(:instance) { controller.new }
|
21
21
|
|
22
22
|
it 'creates a method with the given name' do
|
23
|
-
instance.methods.should include(
|
23
|
+
instance.methods.should include(:resource)
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'prevents the method from being a callable action' do
|
@@ -9,6 +9,12 @@ class Resource
|
|
9
9
|
def initialize(*args); end
|
10
10
|
end
|
11
11
|
|
12
|
+
class Equipment
|
13
|
+
def self.scoped(opts); self; end
|
14
|
+
def self.find(*args); end
|
15
|
+
def initialize(*args); end
|
16
|
+
end
|
17
|
+
|
12
18
|
describe "Rails' integration:", DecentExposure do
|
13
19
|
let(:controller) { Class.new(ActionController::Base) }
|
14
20
|
let(:instance) { controller.new }
|
@@ -65,9 +71,9 @@ describe "Rails' integration:", DecentExposure do
|
|
65
71
|
end
|
66
72
|
|
67
73
|
context 'when no collection method exists' do
|
68
|
-
it '
|
74
|
+
it 'operates directly on the class' do
|
75
|
+
Resource.should_receive(:find)
|
69
76
|
instance.resource
|
70
|
-
instance.methods.should include('resources')
|
71
77
|
end
|
72
78
|
end
|
73
79
|
|
@@ -128,3 +134,22 @@ describe "Rails' integration:", DecentExposure do
|
|
128
134
|
end
|
129
135
|
end
|
130
136
|
end
|
137
|
+
describe "Rails' integration:", DecentExposure do
|
138
|
+
let(:controller) { Class.new(ActionController::Base) }
|
139
|
+
let(:instance) { controller.new }
|
140
|
+
let(:request) { mock(:get? => true) }
|
141
|
+
let(:params) { HashWithIndifferentAccess.new(:resource_id => 42) }
|
142
|
+
|
143
|
+
before do
|
144
|
+
controller.expose(:equipment)
|
145
|
+
instance.stubs(:request).returns(request)
|
146
|
+
instance.stubs(:params).returns(params)
|
147
|
+
end
|
148
|
+
context 'when collection name is same as resource name' do
|
149
|
+
it 'does not create a collection method' do
|
150
|
+
instance.equipment
|
151
|
+
instance.methods.should include(:equipment)
|
152
|
+
instance.methods.should_not include(:equipments)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
metadata
CHANGED
@@ -6,8 +6,8 @@ version: !ruby/object:Gem::Version
|
|
6
6
|
- 1
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 1.0.0.
|
9
|
+
- rc2
|
10
|
+
version: 1.0.0.rc2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Stephen Caudill
|
@@ -16,27 +16,29 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-
|
19
|
+
date: 2010-12-02 00:00:00 -05:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
23
23
|
name: rspec
|
24
24
|
prerelease: false
|
25
25
|
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
26
27
|
requirements:
|
27
28
|
- - ">="
|
28
29
|
- !ruby/object:Gem::Version
|
29
30
|
segments:
|
30
31
|
- 1
|
31
|
-
-
|
32
|
-
-
|
33
|
-
version: 1.
|
32
|
+
- 3
|
33
|
+
- 0
|
34
|
+
version: 1.3.0
|
34
35
|
type: :development
|
35
36
|
version_requirements: *id001
|
36
37
|
- !ruby/object:Gem::Dependency
|
37
38
|
name: mocha
|
38
39
|
prerelease: false
|
39
40
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
40
42
|
requirements:
|
41
43
|
- - ">="
|
42
44
|
- !ruby/object:Gem::Version
|
@@ -51,6 +53,7 @@ dependencies:
|
|
51
53
|
name: actionpack
|
52
54
|
prerelease: false
|
53
55
|
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
54
57
|
requirements:
|
55
58
|
- - ">="
|
56
59
|
- !ruby/object:Gem::Version
|
@@ -66,6 +69,7 @@ executables: []
|
|
66
69
|
extensions: []
|
67
70
|
|
68
71
|
extra_rdoc_files:
|
72
|
+
- README.html
|
69
73
|
- README.md
|
70
74
|
files:
|
71
75
|
- COPYING
|
@@ -75,6 +79,10 @@ files:
|
|
75
79
|
- lib/decent_exposure/default_exposure.rb
|
76
80
|
- lib/decent_exposure/railtie.rb
|
77
81
|
- rails/init.rb
|
82
|
+
- README.html
|
83
|
+
- spec/helper.rb
|
84
|
+
- spec/lib/decent_exposure_spec.rb
|
85
|
+
- spec/lib/rails_integration_spec.rb
|
78
86
|
has_rdoc: true
|
79
87
|
homepage: http://github.com/voxdolo/decent_exposure
|
80
88
|
licenses: []
|
@@ -85,6 +93,7 @@ rdoc_options:
|
|
85
93
|
require_paths:
|
86
94
|
- lib
|
87
95
|
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
88
97
|
requirements:
|
89
98
|
- - ">="
|
90
99
|
- !ruby/object:Gem::Version
|
@@ -92,6 +101,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
101
|
- 0
|
93
102
|
version: "0"
|
94
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
95
105
|
requirements:
|
96
106
|
- - ">"
|
97
107
|
- !ruby/object:Gem::Version
|
@@ -103,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
113
|
requirements: []
|
104
114
|
|
105
115
|
rubyforge_project:
|
106
|
-
rubygems_version: 1.3.
|
116
|
+
rubygems_version: 1.3.7
|
107
117
|
signing_key:
|
108
118
|
specification_version: 3
|
109
119
|
summary: A helper for creating declarative interfaces in controllers
|