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 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 &#39;decent_exposure&#39;</code></pre>
24
+
25
+ <p>When used in Rails 3:</p>
26
+
27
+ <p>In <code>Gemfile</code>:</p>
28
+
29
+ <pre><code>gem &#39;decent_exposure&#39;</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&#8217;s take the following scenario:</p>
58
+
59
+ <pre><code>class ProductsController &lt; 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 =&gt; 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&#8217;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 &lt; 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 (&#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>
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&#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>
118
+ </body>
119
+ </html>
data/README.md CHANGED
@@ -1,15 +1,11 @@
1
- DecentExposure
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. DecentExposure gives you a declarative manner of exposing an interface to the
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 intuit which resource you want 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 you want an instance of. Almost every controller has one of these. In the
53
- RESTful controller paradigm, you might use this in `#show`, `#edit`, `#update`
54
- or `#destroy`.
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
- In the slightly more complicated scenario, you need to find an instance of an
57
- object which doesn't map cleanly to `Object#find`:
74
+ or (depending on the contents of the `params` hash) this:
58
75
 
59
- expose(:product){ category.products.find(params[:id]) }
76
+ Category.find(params[:category_id]).products.new(params[:product])
60
77
 
61
- In the RESTful controller paradigm, you'll again find yourself using this in
62
- `#show`, `#edit`, `#update` or `#destroy`.
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
- DecentExposure provides opinionated default logic when `expose` is invoked without
89
- a block. It's possible, however, to override this with your own custom default
90
- logic by passing a block accepting a single argument to the `default_exposure`
91
- method inside of a controller. The argument will be the string or symbol passed
92
- in to the `expose` call.
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 an exceptionally simple tool, which provides a solitary solution. It
108
- must be used in conjunction with solid design approaches ("Program to an
109
- interface, not an implementation.") and accepted best practices (e.g. Fat Model,
110
- Skinny Controller). In itself, it won't heal a bad design. It is meant only to
111
- be a tool to use in improving the overall design of a Ruby on Rails system and
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
- `DecentExposure` has been developed with the philosophy that Ruby developers shouldn't
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
- * walk-through of an actual implementation (using an existing, popular OSS Rails
131
- app as an example refactor).
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.rc1
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
- self._resource_name = name.to_s
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
- _proxy.find(id).tap do |r|
15
+ proxy.find(id).tap do |r|
10
16
  r.attributes = params[name] unless request.get?
11
17
  end
12
18
  else
13
- _proxy.new(params[name])
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('resource')
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 'creates a collection method to scope from' do
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
- - rc1
10
- version: 1.0.0.rc1
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-07-16 00:00:00 -04:00
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
- - 2
32
- - 9
33
- version: 1.2.9
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.6
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