class-action 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/ClassAction.sublime-project +10 -0
- data/ClassAction.sublime-workspace +1873 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +168 -0
- data/Rakefile +1 -0
- data/class-action.gemspec +27 -0
- data/lib/class-action.rb +2 -0
- data/lib/class_action/action.rb +178 -0
- data/lib/class_action/version.rb +3 -0
- data/lib/class_action.rb +105 -0
- data/spec/class_action/action_spec.rb +264 -0
- data/spec/class_action_spec.rb +185 -0
- data/spec/spec_helper.rb +15 -0
- metadata +135 -0
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Joost Lubach
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# ClassAction
|
2
|
+
|
3
|
+
This gem allows you to write controller actions as classes rather than methods. This is particularly useful for those actions that are too complex, and may require a lot of support methods.
|
4
|
+
|
5
|
+
Within your action class, you may access controller methods, and you can access assignment instance variables.
|
6
|
+
|
7
|
+
Additional benefits include:
|
8
|
+
|
9
|
+
* Action-specific helper methods
|
10
|
+
* Support for responders (future support)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'class-action'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install class-action
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
### Setting up your controller
|
29
|
+
|
30
|
+
In your controller, make sure you have included `ClassAction`, and declare which class actions you wish to use.
|
31
|
+
|
32
|
+
class PostsController
|
33
|
+
include ClassAction
|
34
|
+
|
35
|
+
class_action :show
|
36
|
+
end
|
37
|
+
|
38
|
+
### Create an action
|
39
|
+
|
40
|
+
Then, create your `show` action class (the default is to name this class `PostsController::Show`, but you may customize this).
|
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.
|
45
|
+
|
46
|
+
class PostController
|
47
|
+
class Show < ClassAction::Action
|
48
|
+
|
49
|
+
# We need this method from the controller.
|
50
|
+
controller_method :current_user
|
51
|
+
|
52
|
+
def prepare
|
53
|
+
load_post
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_timestamp
|
57
|
+
@post.last_read_at = DateTime.now
|
58
|
+
@post.last_read_by = current_user
|
59
|
+
end
|
60
|
+
|
61
|
+
def render
|
62
|
+
respond_to do |format|
|
63
|
+
format.html { render @post }
|
64
|
+
format.json { render json: @post }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Note - this method is not executed by ClassAction. It is meant as
|
71
|
+
# support action.
|
72
|
+
def load_post
|
73
|
+
@post = Post.find(params[:id])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Declare a helper method - this helper method is only available for
|
77
|
+
# this action.
|
78
|
+
def current_section
|
79
|
+
params[:section] || @post.sections.first
|
80
|
+
end
|
81
|
+
helper_method :current_section
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
Note that any of your execution methods may call `render` or `redirect`. The execution of the action will stop after any method if it uses these methods (or more formally, when the response body in the controller is set). This removes the need for complex control-flow in your action.
|
87
|
+
|
88
|
+
class Show < ClassAction::Action
|
89
|
+
|
90
|
+
def check_security
|
91
|
+
redirect_to root_path unless authorized?
|
92
|
+
end
|
93
|
+
|
94
|
+
def only_performed_if_authorized
|
95
|
+
render :show
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
def authorized?
|
101
|
+
# Custom logic for this action, perhaps?
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
### Responses
|
107
|
+
|
108
|
+
You can run an action fine like above, where you use `respond_to` or `respond_with` (or even just `render`/`redirect_to`) from within any execution method.
|
109
|
+
|
110
|
+
However, `ClassAction` provides a bit more support for responses. You may define any responders directly in the action:
|
111
|
+
|
112
|
+
class Show < ClassAction::Action
|
113
|
+
|
114
|
+
respond_to :html do
|
115
|
+
render :show
|
116
|
+
end
|
117
|
+
respond_to :json do
|
118
|
+
render :json => @post
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
This employs the use of `ActionController#respond_to`. Additionally, there is support for the Rails 3 style `respond_with`. To illustrate, this:
|
124
|
+
|
125
|
+
class Show < ClassAction::Action
|
126
|
+
|
127
|
+
controller_method :post
|
128
|
+
|
129
|
+
respond_with :post
|
130
|
+
respond_to :html, :json
|
131
|
+
|
132
|
+
respond_to :text do
|
133
|
+
render :text => @post.to_yaml
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
is roughly equivalent to:
|
139
|
+
|
140
|
+
class PostsController < ActionController::Base
|
141
|
+
|
142
|
+
respond_to :html, :json, :text, :only => [ :show ]
|
143
|
+
|
144
|
+
def show
|
145
|
+
respond_with post do |format|
|
146
|
+
format.text do
|
147
|
+
render :text => @post.to_yaml
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
In other words, using `respond_with` in conjunction with `respond_to` allows you to:
|
155
|
+
|
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`.
|
157
|
+
2. Specify the formats that this action responds to. `ClassAction` will make sure that the controller mime types are modified accordingly.
|
158
|
+
3. Create a custom responder block in one breath.
|
159
|
+
|
160
|
+
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
|
+
|
162
|
+
## Contributing
|
163
|
+
|
164
|
+
1. Fork it
|
165
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
166
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
167
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
168
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'class_action/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "class-action"
|
8
|
+
spec.version = ClassAction::VERSION
|
9
|
+
spec.authors = ["Joost Lubach"]
|
10
|
+
spec.email = ["joost@yoazt.com"]
|
11
|
+
spec.description = %q{Allows you to write controller actions as classes, rather than methods.}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'activesupport', '~> 3.2'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
26
|
+
spec.add_development_dependency "actionpack", "~> 3.2"
|
27
|
+
end
|
data/lib/class-action.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
module ClassAction
|
2
|
+
|
3
|
+
# Base class for controller actions.
|
4
|
+
class Action
|
5
|
+
|
6
|
+
######
|
7
|
+
# Initialization
|
8
|
+
|
9
|
+
def initialize(controller)
|
10
|
+
@_controller = controller
|
11
|
+
end
|
12
|
+
|
13
|
+
######
|
14
|
+
# Attributes
|
15
|
+
|
16
|
+
attr_internal_reader :controller
|
17
|
+
|
18
|
+
def available?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
protected :controller, :available?
|
22
|
+
|
23
|
+
######
|
24
|
+
# Controller method exposure
|
25
|
+
|
26
|
+
class << self
|
27
|
+
|
28
|
+
# 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
|
46
|
+
end
|
47
|
+
|
48
|
+
def action_methods
|
49
|
+
methods = public_instance_methods
|
50
|
+
methods -= [ :_execute ]
|
51
|
+
methods -= Object.public_instance_methods
|
52
|
+
methods
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
controller_method :params, :request, :format, sync_assigns: false
|
58
|
+
controller_method :render, :redirect_to, :respond_to, :respond_with
|
59
|
+
|
60
|
+
######
|
61
|
+
# Helper methods
|
62
|
+
|
63
|
+
class << self
|
64
|
+
|
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
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
######
|
84
|
+
# Execution
|
85
|
+
|
86
|
+
def _execute
|
87
|
+
raise ActionNotAvailable unless available?
|
88
|
+
|
89
|
+
# Execute the action by running all public methods in order.
|
90
|
+
self.class.action_methods.each do |method|
|
91
|
+
send method
|
92
|
+
|
93
|
+
# Break execution of the action when some response body is set.
|
94
|
+
# E.g. when the action decides to redirect halfway.
|
95
|
+
break if controller.response_body
|
96
|
+
end
|
97
|
+
|
98
|
+
# Perform a default response if not done so yet.
|
99
|
+
_respond unless controller.response_body
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def _respond
|
105
|
+
copy_assigns_to_controller
|
106
|
+
|
107
|
+
if self.class.respond_with_method
|
108
|
+
response_object = send(self.class.respond_with_method)
|
109
|
+
controller.respond_with response_object, &_respond_block
|
110
|
+
elsif _respond_block
|
111
|
+
controller.respond_to &_respond_block
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def _respond_block
|
116
|
+
responders = self.class.responders
|
117
|
+
return if responders.none? { |format, block| !!block }
|
118
|
+
|
119
|
+
action = self
|
120
|
+
proc do |collector|
|
121
|
+
responders.each do |format, block|
|
122
|
+
next unless block
|
123
|
+
collector.send(format) do
|
124
|
+
action.instance_exec &block
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
######
|
132
|
+
# Responding
|
133
|
+
|
134
|
+
class << self
|
135
|
+
|
136
|
+
attr_accessor :respond_with_method
|
137
|
+
def responders
|
138
|
+
@reponders ||= {}
|
139
|
+
end
|
140
|
+
|
141
|
+
def respond_with(method)
|
142
|
+
self.respond_with_method = method
|
143
|
+
end
|
144
|
+
|
145
|
+
def respond_to(*formats, &block)
|
146
|
+
formats.each do |format|
|
147
|
+
responders[format.to_sym] = block
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def respond_to_any(&block)
|
152
|
+
respond_to :any, &block
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
######
|
158
|
+
# Assigns
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def copy_assigns_from_controller
|
163
|
+
controller.view_assigns.each do |key, value|
|
164
|
+
instance_variable_set "@#{key}", value
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def copy_assigns_to_controller
|
169
|
+
ivars = instance_variables
|
170
|
+
ivars -= [ :@_controller, :@_responders, :@_default_responder ]
|
171
|
+
ivars.each do |ivar|
|
172
|
+
controller.instance_variable_set ivar, instance_variable_get(ivar)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
data/lib/class_action.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module ClassAction
|
4
|
+
|
5
|
+
class ActionNotAvailable < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def included(target)
|
10
|
+
target.extend ClassMethods
|
11
|
+
setup target
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup(target)
|
15
|
+
target.class_eval <<-RUBY, __FILE__, __LINE__+1
|
16
|
+
def class_action
|
17
|
+
@_class_action
|
18
|
+
end
|
19
|
+
RUBY
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def class_action(*actions, klass: nil)
|
26
|
+
actions.each do |action|
|
27
|
+
action_class = klass || const_get(action.to_s.camelize)
|
28
|
+
raise ArgumentError, "ClassAction does not support anonymous classes" if action_class.name.nil?
|
29
|
+
|
30
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
31
|
+
def #{action}
|
32
|
+
_execute_class_action :#{action}, #{action_class.name}
|
33
|
+
end
|
34
|
+
RUBY
|
35
|
+
|
36
|
+
inject_class_action_mimes action.to_s, action_class
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Injects the mimes (formats) that the action responds to into the controller
|
43
|
+
# mimes_for_respond_to hash.
|
44
|
+
def inject_class_action_mimes(action, klass)
|
45
|
+
# If no responders or a default responder is given, we don't do anything.
|
46
|
+
return if klass.responders.empty? || klass.responders.has_key?(:any)
|
47
|
+
|
48
|
+
mimes = mimes_for_respond_to.dup
|
49
|
+
|
50
|
+
# Make sure no extra mimes are allowed for the action.
|
51
|
+
mimes.each do |mime, restrictions|
|
52
|
+
next if klass.responders.key?(mime)
|
53
|
+
exclude_class_action_in_mime_type mime, restrictions, action
|
54
|
+
end
|
55
|
+
|
56
|
+
# Include all action mimes.
|
57
|
+
klass.responders.each do |mime, _block|
|
58
|
+
mimes[mime] ||= { :only => [] }
|
59
|
+
include_class_action_in_mime_type mime, mimes[mime], action
|
60
|
+
end
|
61
|
+
|
62
|
+
self.mimes_for_respond_to = mimes
|
63
|
+
end
|
64
|
+
|
65
|
+
def include_class_action_in_mime_type(mime, restrictions, action)
|
66
|
+
if restrictions && restrictions[:except] && restrictions[:except].include?(action)
|
67
|
+
logger.warn "Warning: action #{action} (ClassAction) responds to `#{mime}` but it does not accept this mime type"
|
68
|
+
elsif restrictions && restrictions[:only] && !restrictions[:only].include?(action)
|
69
|
+
restrictions[:only] << action
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def exclude_class_action_in_mime_type(mime, restrictions, action)
|
74
|
+
restrictions[:except] ||= []
|
75
|
+
restrictions[:except] << action if !restrictions[:except].include?(action)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
def view_context
|
81
|
+
view_context = super
|
82
|
+
|
83
|
+
if class_action
|
84
|
+
# Extend the current view context with the action helpers.
|
85
|
+
view_context.singleton_class.send :include, class_action.class.helpers
|
86
|
+
end
|
87
|
+
|
88
|
+
view_context
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def _execute_class_action(name, klass)
|
94
|
+
@_class_action = klass.new(self)
|
95
|
+
@_class_action._execute
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
if defined?(AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES)
|
101
|
+
AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES << '@_class_action'
|
102
|
+
end
|
103
|
+
|
104
|
+
require 'class_action/version'
|
105
|
+
require 'class_action/action'
|