class-action 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in class-action.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ # File provided as the gem is called 'class-action'.
2
+ require 'class_action'
@@ -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
@@ -0,0 +1,3 @@
1
+ module ClassAction
2
+ VERSION = "0.0.1"
3
+ end
@@ -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'