decent_exposure 1.0.0.rc1 → 1.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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