class-action 0.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15d11311c878ea8b12e8814df141799f8f848650
4
- data.tar.gz: 13b865f9ded36ccff087faaaab5e910fad45515e
3
+ metadata.gz: 2d7c4f6566ad04e2d10d0cfcc3e107302afb367d
4
+ data.tar.gz: 1fc331ef010a59d7723ca3faa206dd72b4322b17
5
5
  SHA512:
6
- metadata.gz: 95a7f24f41a2e6b14db13d23ecd5b776a6de2d65cb7f1fcece692ab79ae53a48a70e65c0a754736aad44596ba7e616f0365b1f14005a550edacfa57f4dcb2509
7
- data.tar.gz: 39e2c284daa56bb1d33f573abe483cc661ec5b6e89108031bfbea315695db13c6da8f9cee3b0e19102023a8dd7060f21befd71467de1ac2e9f06f9d7e002251e
6
+ metadata.gz: 2ae15f473e25cdf638d5aaa51daf9239f61d4ee157544ce55d5184019405795584a8538eb693d0377730117dca662de16fcfdd96e3cbb96e3b3ebcc7c434f83b
7
+ data.tar.gz: 16c0a85b1a3798f913171cecc68830cb5f046d6689a33ddeb316458b85d9168ea019d174fcc2049c6a6358ff3684b722f2c398824d6b4391d59cd9014e6af00b
data/.gitignore CHANGED
@@ -1,17 +1,34 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore .DS_Store
8
+ .DS_Store
9
+
10
+ # Ignore the output files.
11
+ /pkg
12
+
13
+ # Ignore bundler and database config and precompiled assets
14
+ /.bundle
15
+ /.env
16
+
17
+ # RVM files
18
+ .rvmrc
19
+ .ruby-version
20
+ .ruby-gemset
21
+
22
+ # Ignore all logfiles and tempfiles.
23
+ /tmp
24
+
25
+ # Ignore TextMate projects
26
+ *.tmproj
27
+ *.sublime-project
28
+ *.sublime-workspace
29
+
30
+ # Documentation files and other stuff
5
31
  .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
32
+ /doc
33
+ /nbproject
34
+ /coverage
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ script: bundle exec rake
2
+ rvm:
3
+ - 2.0.0
4
+ branches:
5
+ only:
6
+ - master
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ ## 1.1.0 ##
2
+
3
+ * Early class action resolution
4
+
5
+ - Allow access to `class_action` method from before-filters.
6
+
7
+ * Add helper methods onto controller instance as well
8
+
9
+ ## 1.0.0.rc1 ##
10
+
11
+ * Automatic controller method inferral
12
+
13
+ - No need to write `controller_method`
14
+
15
+ * Instance variable references allowed in `respond_with`
16
+
17
+ ## 0.0.2 ##
18
+
19
+ * Extension of responses
20
+
21
+ - Differentiation of responses for XHR requests
22
+ - Differentiation of responses based on state
23
+
24
+ ## 0.0.1 ##
25
+
26
+ * Initial development
27
+
28
+ - Support for execution
29
+ - Rudimentary response support
30
+
31
+ *Joost Lubach*
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ class-action (1.1.0)
5
+ activesupport (~> 3.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionpack (3.2.14)
11
+ activemodel (= 3.2.14)
12
+ activesupport (= 3.2.14)
13
+ builder (~> 3.0.0)
14
+ erubis (~> 2.7.0)
15
+ journey (~> 1.0.4)
16
+ rack (~> 1.4.5)
17
+ rack-cache (~> 1.2)
18
+ rack-test (~> 0.6.1)
19
+ sprockets (~> 2.2.1)
20
+ activemodel (3.2.14)
21
+ activesupport (= 3.2.14)
22
+ builder (~> 3.0.0)
23
+ activesupport (3.2.14)
24
+ i18n (~> 0.6, >= 0.6.4)
25
+ multi_json (~> 1.0)
26
+ builder (3.0.4)
27
+ diff-lcs (1.2.5)
28
+ erubis (2.7.0)
29
+ hike (1.2.3)
30
+ i18n (0.6.9)
31
+ journey (1.0.4)
32
+ multi_json (1.8.2)
33
+ rack (1.4.5)
34
+ rack-cache (1.2)
35
+ rack (>= 0.4)
36
+ rack-test (0.6.2)
37
+ rack (>= 1.0)
38
+ rake (10.1.0)
39
+ rspec (2.14.1)
40
+ rspec-core (~> 2.14.0)
41
+ rspec-expectations (~> 2.14.0)
42
+ rspec-mocks (~> 2.14.0)
43
+ rspec-core (2.14.7)
44
+ rspec-expectations (2.14.4)
45
+ diff-lcs (>= 1.1.3, < 2.0)
46
+ rspec-mocks (2.14.4)
47
+ sprockets (2.2.2)
48
+ hike (~> 1.2)
49
+ multi_json (~> 1.0)
50
+ rack (~> 1.0)
51
+ tilt (~> 1.1, != 1.3.0)
52
+ tilt (1.4.1)
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ actionpack (~> 3.2)
59
+ bundler (~> 1.3)
60
+ class-action!
61
+ rake
62
+ rspec (~> 2.14)
data/README.md CHANGED
@@ -39,16 +39,11 @@ In your controller, make sure you have included `ClassAction`, and declare which
39
39
 
40
40
  Then, create your `show` action class (the default is to name this class `PostsController::Show`, but you may customize this).
41
41
 
42
- All *public* methods are executed in order when the action is run. Any support methods you need, you will need to make protected. You may also declare that you need some controller methods.
43
-
44
- Some default controller methods (`params`, `request`, `render`, `redirect_to`, `respond_to` and `respond_with`) are available at all times.
42
+ All *public* methods are executed in order when the action is run. Any support methods you need, you will need to make protected. Also, all controller methods are available in the action.
45
43
 
46
44
  class PostController
47
45
  class Show < ClassAction::Action
48
46
 
49
- # We need this method from the controller.
50
- controller_method :current_user
51
-
52
47
  def prepare
53
48
  load_post
54
49
  end
@@ -124,9 +119,7 @@ This employs the use of `ActionController#respond_to`. Additionally, there is su
124
119
 
125
120
  class Show < ClassAction::Action
126
121
 
127
- controller_method :post
128
-
129
- respond_with :post
122
+ respond_with :@post
130
123
  respond_to :html, :json
131
124
 
132
125
  respond_to :text do
@@ -142,7 +135,7 @@ is roughly equivalent to:
142
135
  respond_to :html, :json, :text, :only => [ :show ]
143
136
 
144
137
  def show
145
- respond_with post do |format|
138
+ respond_with @post do |format|
146
139
  format.text do
147
140
  render :text => @post.to_yaml
148
141
  end
@@ -151,14 +144,52 @@ is roughly equivalent to:
151
144
 
152
145
  end
153
146
 
147
+ Note that the value you pass to `respond_with` may be a simple symbol (e.g. `:post`) for a method or a reference to an instance variable (e.g. `:@post`).
148
+
154
149
  In other words, using `respond_with` in conjunction with `respond_to` allows you to:
155
150
 
156
- 1. Specify which method to use to obtain the response object (the first argument to `ActionController#respond_with`). Note that this method must exist on the action, or must be exposed using `controller_method`.
151
+ 1. Specify which method to use to obtain the response object (the first argument to `ActionController#respond_with`). Note that this method must exist on the action or controller.
157
152
  2. Specify the formats that this action responds to. `ClassAction` will make sure that the controller mime types are modified accordingly.
158
153
  3. Create a custom responder block in one breath.
159
154
 
160
155
  The only caveat is that you have to specify all your controller-level `respond_to` declarations *before* defining your actions using `class_action`, or you might override the `respond_to` array of your controller.
161
156
 
157
+ ### State based responses
158
+
159
+ In some cases you may want a certain response method (`respond_with`) or responder block (`respond_to`) to be only available in a certain case. For example, in some update action, you may want a different response based on whether the object was saved successfully.
160
+
161
+ Limiting a response method or reponder block this way is possible through the `on:` option in the methods `respond_with` and `respond_to`. The value of this option should correspond to a question-mark method on your action (or controller).
162
+
163
+ For example:
164
+
165
+ class Update < ClassAction::Action
166
+
167
+ respond_with :@post
168
+ respond_to :html, on: :failure do
169
+ render :edit, :status => :unprocessable_entity
170
+ end
171
+
172
+ protected
173
+
174
+ def success?
175
+ @post.errors.blank?
176
+ end
177
+ def failure?
178
+ @post.errors.present?
179
+ end
180
+
181
+ end
182
+
183
+ This will effectively perform the following response logic on the controller:
184
+
185
+ if @post.errors.blank?
186
+ respond_with @post
187
+ elsif @post.errors.present?
188
+ respond_with @post do |format|
189
+ format.html { render :edit, :status => :unprocessable_entity }
190
+ end
191
+ end
192
+
162
193
  ## Contributing
163
194
 
164
195
  1. Fork it
data/class-action.gemspec CHANGED
@@ -23,5 +23,6 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "bundler", "~> 1.3"
24
24
  spec.add_development_dependency "rake"
25
25
  spec.add_development_dependency "rspec", "~> 2.14"
26
+ spec.add_development_dependency "simplecov", "~> 2.14"
26
27
  spec.add_development_dependency "actionpack", "~> 3.2"
27
28
  end
@@ -0,0 +1,2 @@
1
+ # File provided as the gem is called 'class-action'.
2
+ require 'class_action/rspec'
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/object/try'
2
+
1
3
  module ClassAction
2
4
 
3
5
  # Base class for controller actions.
@@ -8,12 +10,15 @@ module ClassAction
8
10
 
9
11
  def initialize(controller)
10
12
  @_controller = controller
13
+ @_controller.singleton_class.send :include, self.class.helpers
11
14
  end
12
15
 
13
16
  ######
14
17
  # Attributes
15
18
 
16
- attr_internal_reader :controller
19
+ def controller
20
+ @_controller
21
+ end
17
22
 
18
23
  def available?
19
24
  true
@@ -26,68 +31,55 @@ module ClassAction
26
31
  class << self
27
32
 
28
33
  # Exposes the given controller methods into the action.
29
- def controller_method(*methods, sync_assigns: true)
30
- if sync_assigns
31
- assigns_copy_to = "copy_assigns_to_controller"
32
- assigns_copy_from = "copy_assigns_from_controller"
33
- end
34
-
35
- methods.each do |method|
36
- class_eval <<-RUBY, __FILE__, __LINE__+1
37
- def #{method}(*args, &block)
38
- #{assigns_copy_to}
39
- controller.send :#{method}, *args, &block
40
- ensure
41
- #{assigns_copy_from}
42
- end
43
- protected :#{method}
44
- RUBY
45
- end
34
+ def _controller_method(method)
35
+ class_eval <<-RUBY, __FILE__, __LINE__+1
36
+ def #{method}(*args, &block)
37
+ copy_assigns_to_controller
38
+ controller.send :#{method}, *args, &block
39
+ ensure
40
+ copy_assigns_from_controller
41
+ end
42
+ protected :#{method}
43
+ RUBY
46
44
  end
47
45
 
48
- def action_methods
49
- methods = public_instance_methods
50
- methods -= [ :_execute ]
51
- methods -= Object.public_instance_methods
52
- methods
53
- end
46
+ end
54
47
 
48
+ def respond_to?(method, include_private = false)
49
+ super || (include_private && controller.respond_to?(method, true))
55
50
  end
56
51
 
57
- controller_method :params, :request, :format, sync_assigns: false
58
- controller_method :render, :redirect_to, :respond_to, :respond_with
52
+ def method_missing(method, *args, &block)
53
+ if controller.respond_to?(method, true)
54
+ self.class._controller_method method
55
+ send method, *args, &block
56
+ else
57
+ super
58
+ end
59
+ end
60
+ private :method_missing
59
61
 
60
62
  ######
61
- # Helper methods
63
+ # Execution
62
64
 
63
65
  class << self
64
66
 
65
- attr_accessor :helpers
66
-
67
- def inherited(klass)
68
- klass.helpers = Module.new
69
- end
70
-
71
- def helper_method(*methods)
72
- methods.each do |method|
73
- helpers.class_eval <<-RUBY, __FILE__, __LINE__+1
74
- def #{method}(*args, &block)
75
- controller.class_action.send(:#{method}, *args, &block)
76
- end
77
- RUBY
78
- end
67
+ def _action_methods
68
+ methods = public_instance_methods
69
+ methods -= [ :_execute ]
70
+ methods -= Object.public_instance_methods
71
+ methods
79
72
  end
80
73
 
81
74
  end
82
75
 
83
- ######
84
- # Execution
85
-
86
76
  def _execute
87
77
  raise ActionNotAvailable unless available?
88
78
 
89
79
  # Execute the action by running all public methods in order.
90
- self.class.action_methods.each do |method|
80
+ self.class._action_methods.each do |method|
81
+ next if self.method(method).arity != 0
82
+
91
83
  send method
92
84
 
93
85
  # Break execution of the action when some response body is set.
@@ -104,8 +96,17 @@ module ClassAction
104
96
  def _respond
105
97
  copy_assigns_to_controller
106
98
 
107
- if self.class.respond_with_method
108
- response_object = send(self.class.respond_with_method)
99
+ response = self.class._responses.find do |on, response|
100
+ !on || send(:"#{on}?")
101
+ end.try(:last)
102
+
103
+ if response
104
+ response_object = if response =~ /^@/
105
+ instance_variable_get(response)
106
+ else
107
+ send(response)
108
+ end
109
+
109
110
  controller.respond_with response_object, &_respond_block
110
111
  elsif _respond_block
111
112
  controller.respond_to &_respond_block
@@ -113,46 +114,95 @@ module ClassAction
113
114
  end
114
115
 
115
116
  def _respond_block
116
- responders = self.class.responders
117
- return if responders.none? { |format, block| !!block }
117
+ responders = {}
118
+ self.class._responders.each do |(format, on), block|
119
+ # Select only those responders that have a block, and for which no precondition is set, or
120
+ # one that matches the current action state.
121
+ responders[format] ||= block if block && (!on || send(:"#{on}?"))
122
+ end
123
+ return if responders.empty?
118
124
 
119
125
  action = self
120
126
  proc do |collector|
121
127
  responders.each do |format, block|
122
- next unless block
123
128
  collector.send(format) do
124
129
  action.instance_exec &block
130
+ copy_assigns_to_controller
125
131
  end
126
132
  end
127
133
  end
128
134
  end
129
135
 
136
+ class << self
130
137
 
131
- ######
132
- # Responding
138
+ ######
139
+ # Helpers
133
140
 
134
- class << self
141
+ attr_accessor :helpers
142
+ def helpers
143
+ @helpers ||= Module.new.tap do |helpers|
144
+ helpers.send :include, superclass.helpers if superclass.respond_to?(:helpers)
145
+ end
146
+ end
135
147
 
136
- attr_accessor :respond_with_method
137
- def responders
138
- @reponders ||= {}
148
+ def helper_method(*methods)
149
+ methods.each do |method|
150
+ helpers.class_eval <<-RUBY, __FILE__, __LINE__+1
151
+ def #{method}(*args, &block)
152
+ controller = if respond_to?(:class_action)
153
+ self
154
+ else
155
+ self.controller
156
+ end
157
+ controller.class_action.send(:#{method}, *args, &block)
158
+ end
159
+ RUBY
160
+ end
161
+ end
162
+
163
+ ######
164
+ # Responders
165
+
166
+ attr_reader :_responders, :_responses
167
+
168
+ def _responses
169
+ @_responses ||= {}.tap do |responses|
170
+ responses.reverse_merge! superclass._responses if superclass.respond_to?(:_responses)
171
+ end
172
+
173
+ # Keep the hash in such an order that the 'nil' condition is always *last*.
174
+ # { :ok => 1, nil => 2, :invalid => 3 } => { :ok => 1, :invalid => 3, nil => 2 }
175
+ @_responses = Hash[ *@_responses.sort_by { |on, _method| on.nil? ? 1 : 0 }.flatten ]
176
+ end
177
+
178
+ def _responders
179
+ @_responders ||= {}.tap do |responders|
180
+ responders.reverse_merge! superclass._responders if superclass.respond_to?(:_responders)
181
+ end
182
+
183
+ # Keep the hash in such an order that the 'nil' conditions are always *last*.
184
+ # { [ (:html, nil) => 1, (:json, nil) => 2, (:html, :ok) => 3 } => { (:html, :ok) => 3, (:html, nil) => 1, (:json, nil) => 2 }
185
+ @_responders = Hash[ *@_responders.sort_by { |(format, on), _method| on.nil? ? 1 : 0 }.inject([]) { |arr, (key, value)| arr << key << value } ]
139
186
  end
140
187
 
141
- def respond_with(method)
142
- self.respond_with_method = method
188
+ # Defines a method that returns the response. Specify an optional precondition in the `on` parameter.
189
+ def respond_with(method, on: nil)
190
+ _responses[on.try(:to_sym)] = method
143
191
  end
144
192
 
145
- def respond_to(*formats, &block)
193
+ # Defines a response block for the given format(s). Specify an optional precondition in the `on` parameter.
194
+ def respond_to(*formats, on: nil, &block)
146
195
  formats.each do |format|
147
- responders[format.to_sym] = block
196
+ _responders[ [format.to_sym, on.try(:to_sym)] ] = block
148
197
  end
149
198
  end
150
199
 
151
- def respond_to_any(&block)
152
- respond_to :any, &block
200
+ # Defines a response block for any remaining format. Specify an optional precondition in the `on` parameter.
201
+ def respond_to_any(on: nil, &block)
202
+ respond_to :any, on: on, &block
153
203
  end
154
204
 
155
- end
205
+ end
156
206
 
157
207
  ######
158
208
  # Assigns
@@ -0,0 +1,57 @@
1
+ module ClassAction
2
+ module RSpec
3
+
4
+ # Adds support for speccing Class Actions. Sets up the example as
5
+ module ClassActionExampleGroup
6
+ def self.included(target)
7
+ target.send :include, ::RSpec::Rails::ControllerExampleGroup
8
+ target.extend ClassMethods
9
+ target.send :include, InstanceMethods
10
+
11
+ target.class_eval do
12
+ # I don't know why ControllerExampleGroup overrides this.
13
+ metadata[:type] = :class_action
14
+
15
+ subject { action }
16
+ before do
17
+ # This is required for response testing, as we won't use
18
+ # ActionController::TestCase#process
19
+ @controller.instance_variable_set '@_response', @response
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def action_class
26
+ described_class
27
+ end
28
+ def controller_class
29
+ # Controller::Action => Controller
30
+ described_class.name.sub(/(.*)::.*$/, '\1').constantize
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ def action
36
+ @action ||= self.class.action_class.new(@controller)
37
+ end
38
+
39
+ def assigns(*)
40
+ action.send :copy_assigns_to_controller
41
+ super
42
+ end
43
+ end
44
+
45
+ def assigns
46
+ @action.send :copy_assigns_to_controller
47
+ super
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
54
+
55
+ RSpec.configure do |c|
56
+ c.include ClassAction::RSpec::ClassActionExampleGroup, type: :class_action
57
+ end
@@ -0,0 +1,67 @@
1
+ module ClassAction
2
+ module RSpec
3
+
4
+ class HaveClassActionMatcher
5
+
6
+ def initialize(action_name)
7
+ @action_name = action_name.to_s
8
+ end
9
+
10
+ def using_class(klass)
11
+ @klass = klass
12
+ self
13
+ end
14
+
15
+ def matches?(controller)
16
+ @controller = controller
17
+ @reason = :unsupported and return false unless controller.respond_to?(:_class_action, true)
18
+ @reason = :not_an_action and return false unless controller.respond_to?(@action_name)
19
+ @reason = :not_a_class_action and return false unless controller.respond_to?(:"_#{@action_name}_action_class", true)
20
+
21
+ if @klass
22
+ @found_class = controller.send(:"_#{@action_name}_action_class").class
23
+ @reason = :incorrect_class and return false if @found_class != @klass
24
+ end
25
+
26
+ true
27
+ end
28
+
29
+ def description
30
+ if @klass
31
+ "have class action :#{@action_name} using class #{@klass}"
32
+ else
33
+ "have class action :#{@action_name}"
34
+ end
35
+ end
36
+
37
+ def failure_message_for_should
38
+ case @reason
39
+ when :unsupported
40
+ "expected controller of class #{@controller.class} to have class action :#{@action_name}, but it does not support class actions"
41
+ when :incorrect_class
42
+ "expected action #{@controller.class}##{@action_name} to use class #{@klass}, but it used #{@found_class}"
43
+ when :not_a_class_action
44
+ "expected action #{@controller.class}##{@action_name} to be a class action"
45
+ else
46
+ "expected controller of class #{@controller.class} to have class action :#{@action_name}"
47
+ end
48
+ end
49
+
50
+ def failure_message_for_should_not
51
+ if @klass
52
+ "expected #{@controller.class}##{@action_name} not to be a class action using class #{@klass}"
53
+ else
54
+ "expected #{@controller.class}##{@action_name} not to be a class action"
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ RSpec::Matchers.module_eval do
64
+ def have_class_action(action_name)
65
+ ClassAction::RSpec::HaveClassActionMatcher.new(action_name)
66
+ end
67
+ end