decent_exposure 1.0.0.rc3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/decent_exposure/default_exposure.rb +5 -1
- data/lib/decent_exposure/version.rb +3 -0
- metadata +32 -28
- data/README.html +0 -119
- data/VERSION +0 -1
- data/rails/init.rb +0 -1
- data/spec/helper.rb +0 -2
- data/spec/lib/decent_exposure_spec.rb +0 -79
- data/spec/lib/rails_integration_spec.rb +0 -155
@@ -2,7 +2,11 @@ module DecentExposure
|
|
2
2
|
module DefaultExposure
|
3
3
|
def self.included(klass)
|
4
4
|
klass.extend(DecentExposure)
|
5
|
-
klass.
|
5
|
+
if klass.respond_to?(:class_attribute)
|
6
|
+
klass.class_attribute(:_default_exposure)
|
7
|
+
else
|
8
|
+
klass.superclass_delegating_accessor(:_default_exposure)
|
9
|
+
end
|
6
10
|
klass.default_exposure do |name|
|
7
11
|
collection = name.to_s.pluralize
|
8
12
|
if respond_to?(collection) && collection != name.to_s && send(collection).respond_to?(:scoped)
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decent_exposure
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.0.rc3
|
9
|
+
version: 1.0.0
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- Stephen Caudill
|
@@ -16,7 +15,7 @@ autorequire:
|
|
16
15
|
bindir: bin
|
17
16
|
cert_chain: []
|
18
17
|
|
19
|
-
date:
|
18
|
+
date: 2011-01-04 00:00:00 -05:00
|
20
19
|
default_executable:
|
21
20
|
dependencies:
|
22
21
|
- !ruby/object:Gem::Dependency
|
@@ -25,13 +24,13 @@ dependencies:
|
|
25
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
26
25
|
none: false
|
27
26
|
requirements:
|
28
|
-
- -
|
27
|
+
- - ~>
|
29
28
|
- !ruby/object:Gem::Version
|
30
29
|
segments:
|
31
|
-
-
|
30
|
+
- 2
|
32
31
|
- 3
|
33
32
|
- 0
|
34
|
-
version:
|
33
|
+
version: 2.3.0
|
35
34
|
type: :development
|
36
35
|
version_requirements: *id001
|
37
36
|
- !ruby/object:Gem::Dependency
|
@@ -40,7 +39,7 @@ dependencies:
|
|
40
39
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
40
|
none: false
|
42
41
|
requirements:
|
43
|
-
- -
|
42
|
+
- - ~>
|
44
43
|
- !ruby/object:Gem::Version
|
45
44
|
segments:
|
46
45
|
- 0
|
@@ -62,27 +61,34 @@ dependencies:
|
|
62
61
|
version: "0"
|
63
62
|
type: :development
|
64
63
|
version_requirements: *id003
|
65
|
-
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: activesupport
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
version_requirements: *id004
|
77
|
+
description: "\n DecentExposure helps you program to an interface, rather than an\n implementation in your Rails controllers. The fact of the matter is that\n sharing state via instance variables in controllers promotes close coupling\n with views. DecentExposure gives you a declarative manner of exposing an\n interface to the state that controllers contain and thereby decreasing\n coupling and improving your testability and overall design.\n "
|
66
78
|
email: scaudill@gmail.com
|
67
79
|
executables: []
|
68
80
|
|
69
81
|
extensions: []
|
70
82
|
|
71
|
-
extra_rdoc_files:
|
72
|
-
|
73
|
-
- README.md
|
83
|
+
extra_rdoc_files: []
|
84
|
+
|
74
85
|
files:
|
75
|
-
- COPYING
|
76
|
-
- README.md
|
77
|
-
- VERSION
|
78
|
-
- lib/decent_exposure.rb
|
79
86
|
- lib/decent_exposure/default_exposure.rb
|
80
87
|
- lib/decent_exposure/railtie.rb
|
81
|
-
-
|
82
|
-
-
|
83
|
-
-
|
84
|
-
-
|
85
|
-
- spec/lib/rails_integration_spec.rb
|
88
|
+
- lib/decent_exposure/version.rb
|
89
|
+
- lib/decent_exposure.rb
|
90
|
+
- README.md
|
91
|
+
- COPYING
|
86
92
|
has_rdoc: true
|
87
93
|
homepage: http://github.com/voxdolo/decent_exposure
|
88
94
|
licenses: []
|
@@ -103,13 +109,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
110
|
none: false
|
105
111
|
requirements:
|
106
|
-
- - "
|
112
|
+
- - ">="
|
107
113
|
- !ruby/object:Gem::Version
|
108
114
|
segments:
|
109
115
|
- 1
|
110
116
|
- 3
|
111
|
-
-
|
112
|
-
version: 1.3.
|
117
|
+
- 6
|
118
|
+
version: 1.3.6
|
113
119
|
requirements: []
|
114
120
|
|
115
121
|
rubyforge_project:
|
@@ -117,7 +123,5 @@ rubygems_version: 1.3.7
|
|
117
123
|
signing_key:
|
118
124
|
specification_version: 3
|
119
125
|
summary: A helper for creating declarative interfaces in controllers
|
120
|
-
test_files:
|
121
|
-
|
122
|
-
- spec/lib/decent_exposure_spec.rb
|
123
|
-
- spec/lib/rails_integration_spec.rb
|
126
|
+
test_files: []
|
127
|
+
|
data/README.html
DELETED
@@ -1,119 +0,0 @@
|
|
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/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
1.0.0.rc3
|
data/rails/init.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
|
data/spec/helper.rb
DELETED
@@ -1,79 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe DecentExposure do
|
4
|
-
|
5
|
-
class Controller
|
6
|
-
extend DecentExposure
|
7
|
-
def self.helper_method(*args); end
|
8
|
-
def self.hide_action(*args); end
|
9
|
-
def memoizable(arg); arg; end
|
10
|
-
end
|
11
|
-
|
12
|
-
context 'classes extending DecentExposure' do
|
13
|
-
subject { Controller }
|
14
|
-
specify { should respond_to(:expose) }
|
15
|
-
specify { should respond_to(:default_exposure) }
|
16
|
-
end
|
17
|
-
|
18
|
-
context '.expose' do
|
19
|
-
let(:controller) { Class.new(Controller){ expose(:resource) } }
|
20
|
-
let(:instance) { controller.new }
|
21
|
-
|
22
|
-
it 'creates a method with the given name' do
|
23
|
-
instance.should respond_to(:resource)
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'prevents the method from being a callable action' do
|
27
|
-
controller.expects(:hide_action).with(:resources)
|
28
|
-
controller.class_eval { expose(:resources) }
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'declares the method as a helper method' do
|
32
|
-
controller.expects(:helper_method).with(:resources)
|
33
|
-
controller.class_eval { expose(:resources) }
|
34
|
-
end
|
35
|
-
|
36
|
-
context 'custom exposures' do
|
37
|
-
before do
|
38
|
-
controller.class_eval do
|
39
|
-
expose(:resource) { memoizable("I'm a resource!") }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'returns the result of the exposed block from the method' do
|
44
|
-
instance.resource.should == "I'm a resource!"
|
45
|
-
end
|
46
|
-
|
47
|
-
it 'memoizes the value of the created method' do
|
48
|
-
instance.expects(:memoizable).once.returns('value')
|
49
|
-
2.times { instance.resource }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
context '.default_exposure' do
|
54
|
-
let(:defaulted_controller) { Class.new(Controller) }
|
55
|
-
let(:instance) { defaulted_controller.new }
|
56
|
-
context 'when the default_exposure is overridden' do
|
57
|
-
before do
|
58
|
-
defaulted_controller.class_eval do
|
59
|
-
default_exposure { 'default value' }
|
60
|
-
expose(:default)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
it 'uses the overridden default_exposure' do
|
64
|
-
instance.default.should == 'default value'
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
context 'with named arguments' do
|
69
|
-
it 'makes the named arguments available' do
|
70
|
-
defaulted_controller.class_eval do
|
71
|
-
default_exposure {|name| "I got: '#{name}'"}
|
72
|
-
expose :default
|
73
|
-
end
|
74
|
-
instance.default.should == "I got: 'default'"
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
@@ -1,155 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
require 'action_controller'
|
3
|
-
require 'decent_exposure/railtie'
|
4
|
-
DecentExposure::Railtie.insert
|
5
|
-
|
6
|
-
class Resource
|
7
|
-
def self.scoped(opts); self; end
|
8
|
-
def self.find(*args); end
|
9
|
-
def initialize(*args); end
|
10
|
-
end
|
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
|
-
|
18
|
-
describe "Rails' integration:", DecentExposure do
|
19
|
-
let(:controller) { Class.new(ActionController::Base) }
|
20
|
-
let(:instance) { controller.new }
|
21
|
-
let(:request) { mock(:get? => true) }
|
22
|
-
let(:params) { HashWithIndifferentAccess.new(:resource_id => 42) }
|
23
|
-
|
24
|
-
before do
|
25
|
-
controller.expose(:resource)
|
26
|
-
instance.stubs(:request).returns(request)
|
27
|
-
end
|
28
|
-
|
29
|
-
context '.expose' do
|
30
|
-
it 'is available to ActionController::Base' do
|
31
|
-
ActionController::Base.should respond_to(:expose)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
context 'within descendant controllers' do
|
36
|
-
let(:resource_controller) { Class.new(ActionController::Base) }
|
37
|
-
let(:instance) { resource_controller.new }
|
38
|
-
|
39
|
-
before do
|
40
|
-
instance.stubs(:request).returns(request)
|
41
|
-
instance.stubs(:params).returns(params)
|
42
|
-
resource_controller.expose :resource
|
43
|
-
end
|
44
|
-
|
45
|
-
it 'inherits the default_exposure' do
|
46
|
-
Resource.stubs(:find).returns('resource')
|
47
|
-
instance.resource.should == 'resource'
|
48
|
-
end
|
49
|
-
|
50
|
-
it 'allows you to override the default_exposure' do
|
51
|
-
resource_controller.class_eval do
|
52
|
-
default_exposure {|name| name.to_s}
|
53
|
-
expose :overridden
|
54
|
-
end
|
55
|
-
instance.overridden.should == 'overridden'
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'does not override the default in ancestors' do
|
59
|
-
Resource.stubs(:find).returns('preserved')
|
60
|
-
instance.resource.should == 'preserved'
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
context '.default_exposure' do
|
65
|
-
before do
|
66
|
-
instance.stubs(:params).returns(params)
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'is available to ActionController::Base' do
|
70
|
-
ActionController::Base.should respond_to(:default_exposure)
|
71
|
-
end
|
72
|
-
|
73
|
-
context 'when no collection method exists' do
|
74
|
-
it 'operates directly on the class' do
|
75
|
-
Resource.should_receive(:find)
|
76
|
-
instance.resource
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
context 'when a collection method exists' do
|
81
|
-
let(:controller){ Class.new(ActionController::Base) }
|
82
|
-
let(:instance){ controller.new }
|
83
|
-
|
84
|
-
before { class Person < Resource; end }
|
85
|
-
|
86
|
-
context 'and the collection can be scoped' do
|
87
|
-
let(:collection){ mock(:scoped => [self]) }
|
88
|
-
|
89
|
-
before{ controller.expose(:person) }
|
90
|
-
|
91
|
-
it 'uses the existing collection method' do
|
92
|
-
instance.stubs(:people).returns(collection)
|
93
|
-
collection.expects(:new)
|
94
|
-
instance.person
|
95
|
-
end
|
96
|
-
end
|
97
|
-
context 'when the collection can not be scoped' do
|
98
|
-
let(:collection){ mock }
|
99
|
-
|
100
|
-
before{ controller.expose(:person) }
|
101
|
-
|
102
|
-
it 'falls back to the singularized constant' do
|
103
|
-
instance.stubs(:people).returns(collection)
|
104
|
-
Person.expects(:new)
|
105
|
-
instance.person
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
context 'when either :resource_id or :id are present in params' do
|
111
|
-
it "calls find with params[:resource_id] on the resource's class" do
|
112
|
-
Resource.expects(:find).with(42)
|
113
|
-
instance.resource
|
114
|
-
end
|
115
|
-
|
116
|
-
context 'when there is no :resource_id in params' do
|
117
|
-
before { instance.stubs(:params).returns({:id => 73}) }
|
118
|
-
|
119
|
-
it "calls find with params[:id] on the resource's class" do
|
120
|
-
Resource.expects(:find).with(73)
|
121
|
-
instance.resource
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
context 'when there are no ids in params' do
|
126
|
-
before do
|
127
|
-
instance.stubs(:params).returns({:resource => {:name => 'bob'}})
|
128
|
-
end
|
129
|
-
|
130
|
-
it 'calls new with params[:resouce_name]' do
|
131
|
-
Resource.expects(:new).with({:name => 'bob'})
|
132
|
-
instance.resource
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
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.should respond_to(:equipment)
|
152
|
-
instance.should_not respond_to(:equipments)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|